diff --git a/.gitignore b/.gitignore index b1f31aab..c97269f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .DS_Store before_commit +dist/fabric.js diff --git a/.jshintrc b/.jshintrc index e5ac13da..bc589f4d 100644 --- a/.jshintrc +++ b/.jshintrc @@ -9,35 +9,41 @@ "ActiveXObject": true }, - "node": true, - "es5": false, - "browser": true, + "node": true, + "es5": false, + "browser": true, - "boss": false, - "curly": false, - "debug": false, - "devel": false, - "eqeqeq": true, - "eqnull": true, - "evil": true, - "expr": true, - "forin": false, - "immed": true, - "laxbreak": true, - "loopfunc": true, - "multistr": true, - "newcap": true, - "noarg": true, - "noempty": false, - "nonew": false, - "nomen": false, - "onevar": false, - "plusplus": false, - "regexp": false, - "undef": true, - "sub": true, - "strict": false, - "white": false, - "unused": true, - "lastsemic": true -} \ No newline at end of file + "boss": false, + "curly": false, + "debug": false, + "devel": false, + "eqeqeq": true, + "eqnull": true, + "evil": true, + "expr": true, + "forin": false, + "immed": true, + "laxbreak": true, + "loopfunc": true, + "multistr": true, + "newcap": true, + "noarg": true, + "noempty": false, + "nonew": false, + "nomen": false, + "onevar": false, + "plusplus": false, + "regexp": false, + "undef": true, + "sub": true, + "strict": false, + "white": false, + "unused": true, + "lastsemic": true, + + // "maxparams": 4 + // "maxcomplexity": 7 + // "maxlen": 100 + "maxdepth": 4, + "maxstatements": 30 +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d23f97f1..ac7af85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ **Edge** +- [BACK_INCOMPAT] Change default objects' originX/originY to left/top + +- [BACK_INCOMPAT] `fabric.StaticCanvas#backgroundImage` and `fabric.StaticCanvas#overlayImage` are `fabric.Image` instances. `fabric.StaticCanvas#backgroundImageOpacity`, `fabric.StaticCanvas#backgroundImageStretch`, `fabric.StaticCanvas#overlayImageLeft` and `fabric.StaticCanvas#overlayImageTop` were removed. + - [BACK_INCOMPAT] `fabric.Text#backgroundColor` is now `fabric.Object#backgroundColor` - [BACK_INCOMPAT] Remove `fabric.Object#toGrayscale` and `fabric.Object#overlayFill` since they're too specific diff --git a/HEADER.js b/HEADER.js index c0238bac..3c42aa7e 100644 --- a/HEADER.js +++ b/HEADER.js @@ -1,6 +1,6 @@ /*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: "1.3.7" }; +var fabric = fabric || { version: "1.3.12" }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } @@ -11,7 +11,9 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { } else { // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom").jsdom(""); + fabric.document = require("jsdom") + .jsdom(""); + fabric.window = fabric.document.createWindow(); } @@ -25,4 +27,19 @@ fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; * True when in environment that's probably Node.js * @type boolean */ -fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; + + +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + "transform", + "fill", "fill-opacity", "fill-rule", + "opacity", + "stroke", "stroke-dasharray", "stroke-linecap", + "stroke-linejoin", "stroke-miterlimit", + "stroke-opacity", "stroke-width" +]; diff --git a/README.md b/README.md index a8b88392..329265ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ + + + + ### Fabric [![Build Status](https://secure.travis-ci.org/kangax/fabric.js.png?branch=master)](http://travis-ci.org/#!/kangax/fabric.js) + +[![Code Climate](https://codeclimate.com/repos/526a0ed089af7e6cf2001389/badges/d1c922dd1511ffa8a72f/gpa.png)](https://codeclimate.com/repos/526a0ed089af7e6cf2001389/feed) + **Fabric.js** is a framework that makes it easy to work with HTML5 canvas element. It is an **interactive object model** on top of canvas element. It is also an **SVG-to-canvas parser**. @@ -12,14 +19,14 @@ Using Fabric.js, you can create and populate objects on canvas; objects like sim ### Goals -- Unit tested (2000+ tests at the moment) +- Unit tested (2300+ tests at the moment) - Modular (~60 small ["classes", modules, mixins](http://fabricjs.com/docs/)) - Cross-browser - [Fast](https://github.com/kangax/fabric.js/wiki/Focus-on-speed) - Encapsulated in one object - No browser sniffing for critical functionality - Runs under ES5 strict mode -- Runs on a server under [Node.js](http://nodejs.org/) (0.6, 0.8, 0.10) +- Runs on a server under [Node.js](http://nodejs.org/) (0.6, 0.8, 0.10) (see [Node.js limitations](https://github.com/kangax/fabric.js/wiki/Fabric-limitations-in-node.js)) ### Supported browsers @@ -55,14 +62,14 @@ Fabric.js started as a foundation for design editor on [printio.ru](http://print $ node build.js - - Or build a custom distribution file, by passing (comma separated) module names to be included. + 2.1 Or build a custom distribution file, by passing (comma separated) module names to be included. - $ node build.js modules=text,serialization,parser - // or - $ node build.js modules=text - // or - $ node build.js modules=parser,text - // etc. + $ node build.js modules=text,serialization,parser + // or + $ node build.js modules=text + // or + $ node build.js modules=parser,text + // etc. By default (when none of the modules are specified) only basic functionality is included. See the list of modules below for more information on each one of them. @@ -70,11 +77,15 @@ Fabric.js started as a foundation for design editor on [printio.ru](http://print To get minimal distribution with interactivity, make sure to include corresponding module: - $ node build.js modules=interaction + $ node build.js modules=interaction - - You can also include all modules like so: + 2.2 You can also include all modules like so: - $ node build.js modules=ALL + $ node build.js modules=ALL + + 2.3 You can exclude a few modules like so: + + $ node build.js modules=ALL exclude=gestures,image_filters 3. Create a minified distribution file @@ -86,7 +97,15 @@ Fabric.js started as a foundation for design editor on [printio.ru](http://print 4. Enable AMD support via require.js (requires uglify) - $ node build.js requirejs modules=... + $ node build.js requirejs modules=... + +5. Create source map file for better productive debugging (requires uglify or google closure compiler).
More information about [source maps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/). + + $ node build.js sourcemap modules=... + + If you use google closure compiler you have to add `sourceMappingURL` manually at the end of the minified file all.min.js (see issue https://code.google.com/p/closure-compiler/issues/detail?id=941). + + //# sourceMappingURL=all.min.js.map ### Demos @@ -104,7 +123,8 @@ Also see [official 4-part intro series](http://fabricjs.com/articles), [presenta These are the optional modules that could be specified for inclusion, when building custom version of fabric: -- **text** — Adds support for `fabric.Text` +- **text** — Adds support for static text (`fabric.Text`) +- **itext** — Adds support for interactive text (`fabric.IText`) - **serialization** — Adds support for `loadFromJSON`, `loadFromDatalessJSON`, and `clone` methods on `fabric.Canvas` - **interaction** — Adds support for interactive features of fabric — selecting/transforming objects/groups via mouse/touch devices. - **parser** — Adds support for `fabric.parseSVGDocument`, `fabric.loadSVGFromURL`, and `fabric.loadSVGFromString` @@ -122,6 +142,7 @@ Additional flags for build script are: - **no-strict** — Strips "use strict" directives from source - **no-svg-export** — Removes svg exporting functionality - **no-es5-compat** - Removes ES5 compat methods (Array.prototype.*, String.prototype.*, Function.prototype.*) +- **sourcemap** - Generates a sourceMap file and adds the `sourceMappingURL` (only if uglifyjs is used) to `dist/all.min.js` For example: diff --git a/build.js b/build.js index 57731710..15e4b4a4 100644 --- a/build.js +++ b/build.js @@ -2,7 +2,9 @@ var fs = require('fs'), exec = require('child_process').exec; var buildArgs = process.argv.slice(2), - buildArgsAsObject = { }; + buildArgsAsObject = { }, + rootPath = process.cwd(), + distributionPath = 'dist/'; buildArgs.forEach(function(arg) { var key = arg.split('=')[0], @@ -21,6 +23,7 @@ var noStrict = 'no-strict' in buildArgsAsObject; var noSVGExport = 'no-svg-export' in buildArgsAsObject; var noES5Compat = 'no-es5-compat' in buildArgsAsObject; var requirejs = 'requirejs' in buildArgsAsObject ? 'requirejs' : false; +var sourceMap = 'sourcemap' in buildArgsAsObject; // set amdLib var to encourage later support of other AMD systems var amdLib = requirejs; @@ -33,14 +36,24 @@ if (amdLib === 'requirejs' && minifier !== 'uglifyjs') { amdUglifyFlags = " -r 'require,exports,window,fabric' -e window:window,undefined "; } +// if we want sourceMap support, uglify or google closure compiler are supported +var sourceMapFlags = ''; +if (sourceMap) { + if (minifier !== 'uglifyjs' && minifier !== 'closure') { + console.log('[notice]: sourceMap support requires uglifyjs or google closure compiler as minifier; changed minifier to uglifyjs.'); + minifier = 'uglifyjs'; + } + sourceMapFlags = minifier === 'uglifyjs' ? ' --source-map all.min.js.map' : ' --create_source_map all.min.js.map --source_map_format=V3'; +} + if (minifier === 'yui') { - mininfierCmd = 'java -jar lib/yuicompressor-2.4.6.jar dist/all.js -o dist/all.min.js'; + mininfierCmd = 'java -jar ' + rootPath + '/lib/yuicompressor-2.4.6.jar all.js -o all.min.js'; } else if (minifier === 'closure') { - mininfierCmd = 'java -jar lib/google_closure_compiler.jar --js dist/all.js --js_output_file dist/all.min.js'; + mininfierCmd = 'java -jar ' + rootPath + '/lib/google_closure_compiler.jar --js all.js --js_output_file all.min.js' + sourceMapFlags; } else if (minifier === 'uglifyjs') { - mininfierCmd = 'uglifyjs ' + amdUglifyFlags + ' --output dist/all.min.js dist/all.js'; + mininfierCmd = 'uglifyjs ' + amdUglifyFlags + ' --output all.min.js all.js' + sourceMapFlags; } var buildSh = 'build-sh' in buildArgsAsObject; @@ -58,6 +71,8 @@ var distFileContents = (noSVGExport ? ' no-svg-export' : '') + (noES5Compat ? ' no-es5-compat' : '') + (requirejs ? ' requirejs' : '') + + (sourceMap ? ' sourcemap' : '') + + ' minifier=' + minifier + '` */'; function appendFileContents(fileNames, callback) { @@ -129,11 +144,11 @@ var filesToInclude = [ ifSpecifiedDependencyInclude('serialization', 'json', 'lib/json2.js'), ifSpecifiedInclude('gestures', 'lib/event.js'), - 'src/log.js', 'src/mixins/observable.mixin.js', 'src/mixins/collection.mixin.js', 'src/util/misc.js', + 'src/util/arc.js', 'src/util/lang_array.js', 'src/util/lang_object.js', 'src/util/lang_string.js', @@ -144,11 +159,14 @@ var filesToInclude = [ 'src/util/dom_misc.js', 'src/util/dom_request.js', + 'src/log.js', + ifSpecifiedInclude('animation', 'src/util/animate.js'), //'src/util/animate.js', ifSpecifiedInclude('easing', 'src/util/anim_ease.js'), ifSpecifiedInclude('parser', 'src/parser.js'), + ifSpecifiedInclude('parser', 'src/elements_parser.js'), 'src/point.class.js', 'src/intersection.class.js', @@ -169,6 +187,7 @@ var filesToInclude = [ ifSpecifiedInclude('interaction', 'src/canvas.class.js'), ifSpecifiedInclude('interaction', 'src/mixins/canvas_events.mixin.js'), + ifSpecifiedInclude('interaction', 'src/mixins/canvas_grouping.mixin.js'), 'src/mixins/canvas_dataurl_exporter.mixin.js', @@ -178,6 +197,8 @@ var filesToInclude = [ 'src/shapes/object.class.js', 'src/mixins/object_origin.mixin.js', 'src/mixins/object_geometry.mixin.js', + 'src/mixins/object_stacking.mixin.js', + 'src/mixins/object.svg_export.js', 'src/mixins/stateful.mixin.js', ifSpecifiedInclude('interaction', 'src/mixins/object_interactivity.mixin.js'), @@ -216,6 +237,12 @@ var filesToInclude = [ ifSpecifiedInclude('text', 'src/shapes/text.class.js'), ifSpecifiedInclude('cufon', 'src/shapes/text.cufon.js'), + ifSpecifiedInclude('itext', 'src/shapes/itext.class.js'), + ifSpecifiedInclude('itext', 'src/mixins/itext_behavior.mixin.js'), + ifSpecifiedInclude('itext', 'src/mixins/itext_click_behavior.mixin.js'), + ifSpecifiedInclude('itext', 'src/mixins/itext_key_behavior.mixin.js'), + ifSpecifiedInclude('itext', 'src/mixins/itext.svg_export.js'), + ifSpecifiedInclude('node', 'src/node.js'), ifSpecifiedAMDInclude(amdLib) @@ -254,8 +281,11 @@ else if (buildSh) { minFilesStr + ' >> ' + path + '\n') } else { + // Change the current working directory + process.chdir(distributionPath); + appendFileContents(filesToInclude, function() { - fs.writeFile('dist/all.js', distFileContents, function (err) { + fs.writeFile('all.js', distFileContents, function (err) { if (err) { console.log(err); throw err; @@ -263,13 +293,13 @@ else { // add js wrapping in AMD closure for requirejs if necessary if (amdLib !== false) { - exec('uglifyjs dist/all.js ' + amdUglifyFlags + ' -b --output dist/all.js'); + exec('uglifyjs all.js ' + amdUglifyFlags + ' -b --output all.js'); } if (amdLib !== false) { - console.log('Built distribution to dist/all.js (' + amdLib + '-compatible)'); + console.log('Built distribution to ' + distributionPath + 'all.js (' + amdLib + '-compatible)'); } else { - console.log('Built distribution to dist/all.js'); + console.log('Built distribution to ' + distributionPath + 'all.js'); } exec(mininfierCmd, function (error, output) { @@ -277,14 +307,18 @@ else { console.error('Minification failed using', minifier, 'with', mininfierCmd); process.exit(1); } - console.log('Minified using', minifier, 'to dist/all.min.js'); + console.log('Minified using', minifier, 'to ' + distributionPath + 'all.min.js'); - exec('gzip -c dist/all.min.js > dist/all.min.js.gz', function (error, output) { - console.log('Gzipped to dist/all.min.js.gz'); + if (sourceMapFlags) { + console.log('Built sourceMap to ' + distributionPath + 'all.min.js.map'); + } + + exec('gzip -c all.min.js > all.min.js.gz', function (error, output) { + console.log('Gzipped to ' + distributionPath + 'all.min.js.gz'); }); }); - // Always build requirejs AMD module in dist/all.require.js + // Always build requirejs AMD module in all.require.js // add necessary requirejs footer code to filesToInclude if we haven't before if (amdLib === false) { amdLib = "requirejs"; @@ -292,19 +326,16 @@ else { } appendFileContents(filesToInclude, function() { - fs.writeFile('dist/all.require.js', distFileContents, function (err) { + fs.writeFile('all.require.js', distFileContents, function (err) { if (err) { console.log(err); throw err; } - exec('uglifyjs dist/all.require.js ' + amdUglifyFlags + ' -b --output dist/all.require.js'); - console.log('Built distribution to dist/all.require.js (requirejs-compatible)'); + exec('uglifyjs all.require.js ' + amdUglifyFlags + ' -b --output all.require.js'); + console.log('Built distribution to ' + distributionPath + 'all.require.js (requirejs-compatible)'); }); }); }); }); - - - } diff --git a/component.json b/component.json index 42bef611..a9ab710d 100644 --- a/component.json +++ b/component.json @@ -2,7 +2,7 @@ "name": "fabric.js", "repo": "kangax/fabric.js", "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.", - "version": "1.3.7", + "version": "1.3.12", "keywords": ["canvas", "graphic", "graphics", "SVG", "node-canvas", "parser", "HTML5", "object model"], "dependencies": {}, "development": {}, diff --git a/create_build_script.js b/create_build_script.js index 6e7ac52f..372cec95 100644 --- a/create_build_script.js +++ b/create_build_script.js @@ -20,7 +20,7 @@ var modules = [ // http://stackoverflow.com/questions/5752002/find-all-possible-subset-combos-in-an-array var combine = function(a, min) { var fn = function(n, src, got, all) { - if (n == 0) { + if (n === 0) { if (got.length > 0) { all[all.length] = got; } @@ -30,14 +30,14 @@ var combine = function(a, min) { fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all); } return; - } + }; var all = []; for (var i = min, _len = a.length; i < _len; i++) { fn(i, a, [], all); } all.push(a); return all; -} +}; var combinations = combine(modules, 1); var startTime = new Date; diff --git a/dist/all.js b/dist/all.js index 397041a6..00176ad0 100644 --- a/dist/all.js +++ b/dist/all.js @@ -1,7 +1,7 @@ -/* build: `node build.js modules=ALL` */ +/* build: `node build.js modules=ALL minifier=uglifyjs` */ /*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: "1.3.7" }; +var fabric = fabric || { version: "1.3.12" }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } @@ -12,7 +12,9 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { } else { // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom").jsdom(""); + fabric.document = require("jsdom") + .jsdom(""); + fabric.window = fabric.document.createWindow(); } @@ -26,7 +28,22 @@ fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; * True when in environment that's probably Node.js * @type boolean */ -fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; + + +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + "transform", + "fill", "fill-opacity", "fill-rule", + "opacity", + "stroke", "stroke-dasharray", "stroke-linecap", + "stroke-linejoin", "stroke-miterlimit", + "stroke-opacity", "stroke-width" +]; /*! @@ -1750,1935 +1767,1834 @@ if (!JSON) { }()); /* - ---------------------------------------------------- - Event.js : 1.1.1 : 2012/11/19 : MIT License - ---------------------------------------------------- - https://github.com/mudcube/Event.js - ---------------------------------------------------- - 1 : click, dblclick, dbltap - 1+ : tap, longpress, drag, swipe - 2+ : pinch, rotate - : mousewheel, devicemotion, shake - ---------------------------------------------------- - TODO - ---------------------------------------------------- - * switch configuration to 4th argument on addEventListener - * bbox calculation for elements scaled with transform. - ---------------------------------------------------- - NOTES - ---------------------------------------------------- - * When using other libraries that may have built in "Event" namespace, - i.e. Typescript, you can use "eventjs" instead of "Event" for all example calls. - ---------------------------------------------------- - REQUIREMENTS: querySelector, querySelectorAll - ---------------------------------------------------- - * There are two ways to add/remove events with this library. - ---------------------------------------------------- - // Retains "this" attribute as target, and overrides native addEventListener. - target.addEventListener(type, listener, useCapture); - target.removeEventListener(type, listener, useCapture); + ---------------------------------------------------- + Event.js : 1.1.3 : 2013/07/17 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- + https://github.com/rykerwilliams/Event.js + ---------------------------------------------------- + 1 : click, dblclick, dbltap + 1+ : tap, longpress, drag, swipe + 2+ : pinch, rotate + : mousewheel, devicemotion, shake + ---------------------------------------------------- + Ideas for the future + ---------------------------------------------------- + * GamePad, and other input abstractions. + * Event batching - i.e. for every x fingers down a new gesture is created. + ---------------------------------------------------- + http://www.w3.org/TR/2011/WD-touch-events-20110505/ + ---------------------------------------------------- + +*/ - // Attempts to perform as fast as possible. - Event.add(type, listener, configure); - Event.remove(type, listener, configure); +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(eventjs) === "undefined") var eventjs = Event; - * You can turn prototyping on/off for individual features. - ---------------------------------------------------- - Event.modifyEventListener = true; // add custom *EventListener commands to HTMLElements. - Event.modifySelectors = true; // add bulk *EventListener commands on NodeLists from querySelectorAll and others. +(function(root) { "use strict"; - * Example of setting up a single listener with a custom configuration. - ---------------------------------------------------- - // optional configuration. - var configure = { - fingers: 2, // listen for specifically two fingers. - snap: 90 // snap to 90 degree intervals. - }; - // adding with addEventListener() - target.addEventListener("swipe", function(event) { - // additional variables can be found on the event object. - console.log(event.velocity, event.angle, event.fingers); - }, configure); +// Add custom *EventListener commands to HTMLElements (set false to prevent funkiness). +root.modifyEventListener = true; - // adding with Event.add() - Event.add("swipe", function(event, self) { - // additional variables can be found on the self object. - console.log(self.velocity, self.angle, self.fingers); - }, configure); - - * Multiple listeners glued together. - ---------------------------------------------------- - // adding with addEventListener() - target.addEventListener("click swipe", function(event) { }); - - // adding with Event.add() - Event.add(target, "click swipe", function(event, self) { }); - - * Use query selectors to create an event (querySelectorAll) - ---------------------------------------------------- - // adding events to NodeList from querySelectorAll() - document.querySelectorAll("#element a.link").addEventListener("click", callback); - - // adding with Event.add() - Event.add("#element a.link", "click", callback); - - * Listen for selector to become available (querySelector) - ---------------------------------------------------- - Event.add("body", "ready", callback); - // or... - Event.add({ - target: "body", - type: "ready", - timeout: 10000, // set a timeout to stop checking. - interval: 30, // set how often to check for element. - listener: callback - }); - - * Multiple listeners bound to one callback w/ single configuration. - ---------------------------------------------------- - var bindings = Event.add({ - target: target, - type: "click swipe", - snap: 90, // snap to 90 degree intervals. - minFingers: 2, // minimum required fingers to start event. - maxFingers: 4, // maximum fingers in one event. - listener: function(event, self) { - console.log(self.gesture); // will be click or swipe. - console.log(self.x); - console.log(self.y); - console.log(self.identifier); - console.log(self.start); - console.log(self.fingers); // somewhere between "2" and "4". - self.pause(); // disable event. - self.resume(); // enable event. - self.remove(); // remove event. - } - }); - - * Multiple listeners bound to multiple callbacks w/ single configuration. - ---------------------------------------------------- - var bindings = Event.add({ - target: target, - minFingers: 1, - maxFingers: 12, - listeners: { - click: function(event, self) { - self.remove(); // removes this click listener. - }, - swipe: function(event, self) { - binding.remove(); // removes both the click + swipe listeners. - } - } - }); - - * Multiple listeners bound to multiple callbacks w/ multiple configurations. - ---------------------------------------------------- - var binding = Event.add({ - target: target, - listeners: { - longpress: { - fingers: 1, - wait: 500, // milliseconds - listener: function(event, self) { - console.log(self.fingers); // "1" finger. - } - }, - drag: { - fingers: 3, - position: "relative", // "relative", "absolute", "difference", "move" - listener: function(event, self) { - console.log(self.fingers); // "3" fingers. - console.log(self.x); // coordinate is relative to edge of target. - } - } - } - }); - - * Capturing an event and manually forwarding it to a proxy (tiered events). - ---------------------------------------------------- - Event.add(target, "down", function(event, self) { - var x = event.pageX; // local variables that wont change. - var y = event.pageY; - Event.proxy.drag({ - event: event, - target: target, - listener: function(event, self) { - console.log(x - event.pageX); // measure movement. - console.log(y - event.pageY); - } - }); - }); - ---------------------------------------------------- - - * Event proxies. - * type, fingers, state, start, x, y, position, bbox - * rotation, scale, velocity, angle, delay, timeout - ---------------------------------------------------- - // "Click" :: fingers, minFingers, maxFingers. - Event.add(window, "click", function(event, self) { - console.log(self.gesture, self.x, self.y); - }); - // "Double-Click" :: fingers, minFingers, maxFingers. - Event.add(window, "dblclick", function(event, self) { - console.log(self.gesture, self.x, self.y); - }); - // "Drag" :: fingers, maxFingers, position - Event.add(window, "drag", function(event, self) { - console.log(self.gesture, self.fingers, self.state, self.start, self.x, self.y, self.bbox); - }); - // "Gesture" :: fingers, minFingers, maxFingers. - Event.add(window, "gesture", function(event, self) { - console.log(self.gesture, self.fingers, self.state, self.rotation, self.scale); - }); - // "Swipe" :: fingers, minFingers, maxFingers, snap, threshold. - Event.add(window, "swipe", function(event, self) { - console.log(self.gesture, self.fingers, self.velocity, self.angle, self.start, self.x, self.y); - }); - // "Tap" :: fingers, minFingers, maxFingers, timeout. - Event.add(window, "tap", function(event, self) { - console.log(self.gesture, self.fingers); - }); - // "Longpress" :: fingers, minFingers, maxFingers, delay. - Event.add(window, "longpress", function(event, self) { - console.log(self.gesture, self.fingers); - }); - // - Event.add(window, "shake", function(event, self) { - console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); - }); - // - Event.add(window, "devicemotion", function(event, self) { - console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); - }); - // - Event.add(window, "wheel", function(event, self) { - console.log(self.gesture, self.state, self.wheelDelta); - }); - - * Stop, prevent and cancel. - ---------------------------------------------------- - Event.stop(event); // stop bubble. - Event.prevent(event); // prevent default. - Event.cancel(event); // stop and prevent. - - * Track for proper command/control-key for Mac/PC. - ---------------------------------------------------- - Event.add(window, "keyup keydown", Event.proxy.metaTracker); - console.log(Event.proxy.metaKey); - - * Test for event features, in this example Drag & Drop file support. - ---------------------------------------------------- - console.log(Event.supports('dragstart') && Event.supports('drop') && !!window.FileReader); - - */ - -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(eventjs) === "undefined") - var eventjs = Event; - -Event = (function(root) { - "use strict"; - -// Add custom *EventListener commands to HTMLElements. - root.modifyEventListener = false; - -// Add bulk *EventListener commands on NodeLists from querySelectorAll and others. - root.modifySelectors = false; +// Add bulk *EventListener commands on NodeLists from querySelectorAll and others (set false to prevent funkiness). +root.modifySelectors = true; // Event maintenance. - root.add = function(target, type, listener, configure) { - return eventManager(target, type, listener, configure, "add"); - }; +root.add = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "add"); +}; - root.remove = function(target, type, listener, configure) { - return eventManager(target, type, listener, configure, "remove"); - }; +root.remove = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "remove"); +}; - root.stop = function(event) { - if (event.stopPropagation) - event.stopPropagation(); - event.cancelBubble = true; // <= IE8 - event.bubble = 0; - }; +root.stop = function(event) { + if (!event) return; + if (event.stopPropagation) event.stopPropagation(); + event.cancelBubble = true; // <= IE8 + event.bubble = 0; +}; - root.prevent = function(event) { - if (event.preventDefault) - event.preventDefault(); - event.returnValue = false; // <= IE8 - }; +root.prevent = function(event) { + if (!event) return; + if (event.preventDefault) event.preventDefault(); + if (event.preventManipulation) event.preventManipulation(); // MS + event.returnValue = false; // <= IE8 +}; - root.cancel = function(event) { - root.stop(event); - root.prevent(event); - }; +root.cancel = function(event) { + root.stop(event); + root.prevent(event); +}; // Check whether event is natively supported (via @kangax) - root.supports = function(target, type) { - if (typeof(target) === "string") { - type = target; - target = window; - } - type = "on" + type; - if (type in target) - return true; - if (!target.setAttribute) - target = document.createElement("div"); - if (target.setAttribute && target.removeAttribute) { - target.setAttribute(type, ""); - var isSupported = typeof target[type] === "function"; - if (typeof target[type] !== "undefined") - target[type] = null; - target.removeAttribute(type); - return isSupported; - } - }; +root.getEventSupport = function (target, type) { + if (typeof(target) === "string") { + type = target; + target = window; + } + type = "on" + type; + if (type in target) return true; + if (!target.setAttribute) target = document.createElement("div"); + if (target.setAttribute && target.removeAttribute) { + target.setAttribute(type, ""); + var isSupported = typeof target[type] === "function"; + if (typeof target[type] !== "undefined") target[type] = null; + target.removeAttribute(type); + return isSupported; + } +}; - var clone = function(obj) { - if (!obj || typeof (obj) !== 'object') - return obj; - var temp = new obj.constructor(); - for (var key in obj) { - if (!obj[key] || typeof (obj[key]) !== 'object') { - temp[key] = obj[key]; - } else { // clone sub-object - temp[key] = clone(obj[key]); - } - } - return temp; - }; +var clone = function (obj) { + if (!obj || typeof (obj) !== 'object') return obj; + var temp = new obj.constructor(); + for (var key in obj) { + if (!obj[key] || typeof (obj[key]) !== 'object') { + temp[key] = obj[key]; + } else { // clone sub-object + temp[key] = clone(obj[key]); + } + } + return temp; +}; /// Handle custom *EventListener commands. - var eventManager = function(target, type, listener, configure, trigger, fromOverwrite) { - configure = configure || {}; - // Check for element to load on interval (before onload). - if (typeof(target) === "string" && type === "ready") { - var time = (new Date()).getTime(); - var timeout = configure.timeout; - var ms = configure.interval || 1000 / 60; - var interval = window.setInterval(function() { - if ((new Date()).getTime() - time > timeout) { - window.clearInterval(interval); - } - if (document.querySelector(target)) { - window.clearInterval(interval); - listener(); - } - }, ms); - return; - } - // Get DOM element from Query Selector. - if (typeof(target) === "string") { - target = document.querySelectorAll(target); - if (target.length === 0) - return createError("Missing target on listener!"); // No results. - if (target.length === 1) { // Single target. - target = target[0]; - } - } - /// Handle multiple targets. - var event; - var events = {}; - if (target.length > 0) { - for (var n0 = 0, length0 = target.length; n0 < length0; n0++) { - event = eventManager(target[n0], type, listener, clone(configure), trigger); - if (event) - events[n0] = event; - } - return createBatchCommands(events); - } - // Check for multiple events in one string. - if (type.indexOf && type.indexOf(" ") !== -1) - type = type.split(" "); - if (type.indexOf && type.indexOf(",") !== -1) - type = type.split(","); - // Attach or remove multiple events associated with a target. - if (typeof(type) !== "string") { // Has multiple events. - if (typeof(type.length) === "number") { // Handle multiple listeners glued together. - for (var n1 = 0, length1 = type.length; n1 < length1; n1++) { // Array [type] - event = eventManager(target, type[n1], listener, clone(configure), trigger); - if (event) - events[type[n1]] = event; - } - } else { // Handle multiple listeners. - for (var key in type) { // Object {type} - if (typeof(type[key]) === "function") { // without configuration. - event = eventManager(target, key, type[key], clone(configure), trigger); - } else { // with configuration. - event = eventManager(target, key, type[key].listener, clone(type[key]), trigger); - } - if (event) - events[key] = event; - } - } - return createBatchCommands(events); - } - // Ensure listener is a function. - if (typeof(listener) !== "function") - return createError("Listener is not a function!"); - // Generate a unique wrapper identifier. - var useCapture = configure.useCapture || false; - var id = normalize(type) + getID(target) + "." + getID(listener) + "." + (useCapture ? 1 : 0); - // Handle the event. - if (root.Gesture && root.Gesture._gestureHandlers[type]) { // Fire custom event. - if (trigger === "remove") { // Remove event listener. - if (!wrappers[id]) - return; // Already removed. - wrappers[id].remove(); - delete wrappers[id]; - } else if (trigger === "add") { // Attach event listener. - if (wrappers[id]) - return wrappers[id]; // Already attached. - // Retains "this" orientation. - if (configure.useCall && !root.modifyEventListener) { - var tmp = listener; - listener = function(event, self) { - for (var key in self) - event[key] = self[key]; - return tmp.call(target, event); - }; - } - // Create listener proxy. - configure.gesture = type; - configure.target = target; - configure.listener = listener; - configure.fromOverwrite = fromOverwrite; - // Record wrapper. - wrappers[id] = root.proxy[type](configure); - } - } else { // Fire native event. - type = normalize(type); - if (trigger === "remove") { // Remove event listener. - if (!wrappers[id]) - return; // Already removed. - target[remove](type, listener, useCapture); - delete wrappers[id]; - } else if (trigger === "add") { // Attach event listener. - if (wrappers[id]) - return wrappers[id]; // Already attached. - target[add](type, listener, useCapture); - // Record wrapper. - wrappers[id] = { - type: type, - target: target, - listener: listener, - remove: function() { - root.remove(target, type, listener, configure); - } - }; - } - } - return wrappers[id]; - }; +var eventManager = function(target, type, listener, configure, trigger, fromOverwrite) { + configure = configure || {}; + // Check whether target is a configuration variable; + if (String(target) === "[object Object]") { + var data = target; + target = data.target; + type = data.type; + listener = data.listener; + delete data.target; + delete data.type; + delete data.listener; + for (var key in data) { + configure[key] = data[key]; + } + } + /// + if (!target || !type || !listener) return; + // Check for element to load on interval (before onload). + if (typeof(target) === "string" && type === "ready") { + var time = (new Date()).getTime(); + var timeout = configure.timeout; + var ms = configure.interval || 1000 / 60; + var interval = window.setInterval(function() { + if ((new Date()).getTime() - time > timeout) { + window.clearInterval(interval); + } + if (document.querySelector(target)) { + window.clearInterval(interval); + setTimeout(listener, 1); + } + }, ms); + return; + } + // Get DOM element from Query Selector. + if (typeof(target) === "string") { + target = document.querySelectorAll(target); + if (target.length === 0) return createError("Missing target on listener!", arguments); // No results. + if (target.length === 1) { // Single target. + target = target[0]; + } + } + + /// Handle multiple targets. + var event; + var events = {}; + if (target.length > 0 && target !== window) { + for (var n0 = 0, length0 = target.length; n0 < length0; n0 ++) { + event = eventManager(target[n0], type, listener, clone(configure), trigger); + if (event) events[n0] = event; + } + return createBatchCommands(events); + } + // Check for multiple events in one string. + if (type.indexOf && type.indexOf(" ") !== -1) type = type.split(" "); + if (type.indexOf && type.indexOf(",") !== -1) type = type.split(","); + // Attach or remove multiple events associated with a target. + if (typeof(type) !== "string") { // Has multiple events. + if (typeof(type.length) === "number") { // Handle multiple listeners glued together. + for (var n1 = 0, length1 = type.length; n1 < length1; n1 ++) { // Array [type] + event = eventManager(target, type[n1], listener, clone(configure), trigger); + if (event) events[type[n1]] = event; + } + } else { // Handle multiple listeners. + for (var key in type) { // Object {type} + if (typeof(type[key]) === "function") { // without configuration. + event = eventManager(target, key, type[key], clone(configure), trigger); + } else { // with configuration. + event = eventManager(target, key, type[key].listener, clone(type[key]), trigger); + } + if (event) events[key] = event; + } + } + return createBatchCommands(events); + } + // Ensure listener is a function. + if (typeof(target) !== "object") return createError("Target is not defined!", arguments); + if (typeof(listener) !== "function") return createError("Listener is not a function!", arguments); + // Generate a unique wrapper identifier. + var useCapture = configure.useCapture || false; + var id = getID(target) + "." + getID(listener) + "." + (useCapture ? 1 : 0); + // Handle the event. + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // Fire custom event. + id = type + id; + if (trigger === "remove") { // Remove event listener. + if (!wrappers[id]) return; // Already removed. + wrappers[id].remove(); + delete wrappers[id]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[id]) { + wrappers[id].add(); + return wrappers[id]; // Already attached. + } + // Retains "this" orientation. + if (configure.useCall && !root.modifyEventListener) { + var tmp = listener; + listener = function(event, self) { + for (var key in self) event[key] = self[key]; + return tmp.call(target, event); + }; + } + // Create listener proxy. + configure.gesture = type; + configure.target = target; + configure.listener = listener; + configure.fromOverwrite = fromOverwrite; + // Record wrapper. + wrappers[id] = root.proxy[type](configure); + } + return wrappers[id]; + } else { // Fire native event. + var eventList = getEventList(type); + for (var n = 0, eventId; n < eventList.length; n ++) { + type = eventList[n]; + eventId = type + "." + id; + if (trigger === "remove") { // Remove event listener. + if (!wrappers[eventId]) continue; // Already removed. + target[remove](type, listener, useCapture); + delete wrappers[eventId]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[eventId]) return wrappers[eventId]; // Already attached. + target[add](type, listener, useCapture); + // Record wrapper. + wrappers[eventId] = { + id: eventId, + type: type, + target: target, + listener: listener, + remove: function() { + for (var n = 0; n < eventList.length; n ++) { + root.remove(target, eventList[n], listener, configure); + } + } + }; + } + } + return wrappers[eventId]; + } +}; /// Perform batch actions on multiple events. - var createBatchCommands = function(events) { - return { - remove: function() { // Remove multiple events. - for (var key in events) { - events[key].remove(); - } - }, - add: function() { // Add multiple events. - for (var key in events) { - events[key].add(); - } - } - }; - }; +var createBatchCommands = function(events) { + return { + remove: function() { // Remove multiple events. + for (var key in events) { + events[key].remove(); + } + }, + add: function() { // Add multiple events. + for (var key in events) { + events[key].add(); + } + } + }; +}; /// Display error message in console. - var createError = function(message) { - if (typeof(console) === "undefined") - return; - if (typeof(console.error) === "undefined") - return; - console.error(message); - }; +var createError = function(message, data) { + if (typeof(console) === "undefined") return; + if (typeof(console.error) === "undefined") return; + console.error(message, data); +}; /// Handle naming discrepancies between platforms. - var normalize = (function() { - var translate = {}; - return function(type) { - if (!root.pointerType) { - if (window.navigator.msPointerEnabled) { - root.pointerType = "mspointer"; - translate = { - "mousedown": "MSPointerDown", - "mousemove": "MSPointerMove", - "mouseup": "MSPointerUp" - }; - } else if (root.supports("touchstart")) { - root.pointerType = "touch"; - translate = { - "mousedown": "touchstart", - "mouseup": "touchend", - "mousemove": "touchmove" - }; - } else { - root.pointerType = "mouse"; - } - } - if (translate[type]) - type = translate[type]; - if (!document.addEventListener) { // IE - return "on" + type; - } else { - return type; - } - }; - })(); +var pointerDefs = { + "msPointer": [ "MSPointerDown", "MSPointerMove", "MSPointerUp" ], + "touch": [ "touchstart", "touchmove", "touchend" ], + "mouse": [ "mousedown", "mousemove", "mouseup" ] +}; + +var pointerDetect = { + // MSPointer + "MSPointerDown": 0, + "MSPointerMove": 1, + "MSPointerUp": 2, + // Touch + "touchstart": 0, + "touchmove": 1, + "touchend": 2, + // Mouse + "mousedown": 0, + "mousemove": 1, + "mouseup": 2 +}; + +var getEventSupport = (function() { + root.supports = {}; + if (window.navigator.msPointerEnabled) { + root.supports.msPointer = true; + } + if (root.getEventSupport("touchstart")) { + root.supports.touch = true; + } + if (root.getEventSupport("mousedown")) { + root.supports.mouse = true; + } +})(); + +var getEventList = (function() { + return function(type) { + var prefix = document.addEventListener ? "" : "on"; // IE + var idx = pointerDetect[type]; + if (isFinite(idx)) { + var types = []; + for (var key in root.supports) { + types.push(prefix + pointerDefs[key][idx]); + } + return types; + } else { + return [ prefix + type ]; + } + }; +})(); /// Event wrappers to keep track of all events placed in the window. - var wrappers = {}; - var counter = 0; - var getID = function(object) { - if (object === window) - return "#window"; - if (object === document) - return "#document"; - if (!object) - return createError("Missing target on listener!"); - if (!object.uniqueID) - object.uniqueID = "id" + counter++; - return object.uniqueID; - }; +var wrappers = {}; +var counter = 0; +var getID = function(object) { + if (object === window) return "#window"; + if (object === document) return "#document"; + if (!object.uniqueID) object.uniqueID = "e" + counter ++; + return object.uniqueID; +}; /// Detect platforms native *EventListener command. - var add = document.addEventListener ? "addEventListener" : "attachEvent"; - var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; +var add = document.addEventListener ? "addEventListener" : "attachEvent"; +var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; - /* - Pointer.js - ------------------------ - Modified from; https://github.com/borismus/pointer.js - */ +/* + Pointer.js + ------------------------ + Modified from; https://github.com/borismus/pointer.js +*/ - root.createPointerEvent = function(event, self, preventRecord) { - var eventName = self.gesture; - var target = self.target; - var pts = event.changedTouches || root.proxy.getCoords(event); - if (pts.length) { - var pt = pts[0]; - self.pointers = preventRecord ? [] : pts; - self.pageX = pt.pageX; - self.pageY = pt.pageY; - self.x = self.pageX; - self.y = self.pageY; - } - /// - var newEvent = document.createEvent("Event"); - newEvent.initEvent(eventName, true, true); - newEvent.originalEvent = event; - for (var k in self) { - if (k === "target") - continue; - newEvent[k] = self[k]; - } - target.dispatchEvent(newEvent); - }; +root.createPointerEvent = function (event, self, preventRecord) { + var eventName = self.gesture; + var target = self.target; + var pts = event.changedTouches || root.proxy.getCoords(event); + if (pts.length) { + var pt = pts[0]; + self.pointers = preventRecord ? [] : pts; + self.pageX = pt.pageX; + self.pageY = pt.pageY; + self.x = self.pageX; + self.y = self.pageY; + } + /// + var newEvent = document.createEvent("Event"); + newEvent.initEvent(eventName, true, true); + newEvent.originalEvent = event; + for (var k in self) { + if (k === "target") continue; + newEvent[k] = self[k]; + } + /// + var type = newEvent.type; + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. +// target.dispatchEvent(newEvent); + self.oldListener.call(target, newEvent, self, false); + } +}; /// Allows *EventListener to use custom event proxies. - if (root.modifyEventListener && window.HTMLElement) - (function() { - var augmentEventListener = function(proto) { - var recall = function(trigger) { // overwrite native *EventListener's - var handle = trigger + "EventListener"; - var handler = proto[handle]; - proto[handle] = function(type, listener, useCapture) { - if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. - var configure = useCapture; - if (typeof(useCapture) === "object") { - configure.useCall = true; - } else { // convert to configuration object. - configure = { - useCall: true, - useCapture: useCapture - }; - } - eventManager(this, type, listener, configure, trigger, true); - handler.call(this, type, listener, useCapture); - } else { // use native function. - handler.call(this, normalize(type), listener, useCapture); - } - }; - }; - recall("add"); - recall("remove"); - }; - // NOTE: overwriting HTMLElement doesn't do anything in Firefox. - if (navigator.userAgent.match(/Firefox/)) { - // TODO: fix Firefox for the general case. - augmentEventListener(HTMLDivElement.prototype); - augmentEventListener(HTMLCanvasElement.prototype); - } else { - augmentEventListener(HTMLElement.prototype); - } - augmentEventListener(document); - augmentEventListener(window); - })(); +if (root.modifyEventListener && window.HTMLElement) (function() { + var augmentEventListener = function(proto) { + var recall = function(trigger) { // overwrite native *EventListener's + var handle = trigger + "EventListener"; + var handler = proto[handle]; + proto[handle] = function (type, listener, useCapture) { + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. + var configure = useCapture; + if (typeof(useCapture) === "object") { + configure.useCall = true; + } else { // convert to configuration object. + configure = { + useCall: true, + useCapture: useCapture + }; + } + eventManager(this, type, listener, configure, trigger, true); +// handler.call(this, type, listener, useCapture); + } else { // use native function. + var types = getEventList(type); + for (var n = 0; n < types.length; n ++) { + handler.call(this, types[n], listener, useCapture); + } + } + }; + }; + recall("add"); + recall("remove"); + }; + // NOTE: overwriting HTMLElement doesn't do anything in Firefox. + if (navigator.userAgent.match(/Firefox/)) { + // TODO: fix Firefox for the general case. + augmentEventListener(HTMLDivElement.prototype); + augmentEventListener(HTMLCanvasElement.prototype); + } else { + augmentEventListener(HTMLElement.prototype); + } + augmentEventListener(document); + augmentEventListener(window); +})(); /// Allows querySelectorAll and other NodeLists to perform *EventListener commands in bulk. - if (root.modifySelectors) - (function() { - var proto = NodeList.prototype; - proto.removeEventListener = function(type, listener, useCapture) { - for (var n = 0, length = this.length; n < length; n++) { - this[n].removeEventListener(type, listener, useCapture); - } - }; - proto.addEventListener = function(type, listener, useCapture) { - for (var n = 0, length = this.length; n < length; n++) { - this[n].addEventListener(type, listener, useCapture); - } - }; - })(); +if (root.modifySelectors) (function() { + var proto = NodeList.prototype; + proto.removeEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n ++) { + this[n].removeEventListener(type, listener, useCapture); + } + }; + proto.addEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n ++) { + this[n].addEventListener(type, listener, useCapture); + } + }; +})(); - return root; +return root; })(Event); /* - ---------------------------------------------------- - Event.proxy : 0.4.2 : 2012/07/29 : MIT License - ---------------------------------------------------- - https://github.com/mudcube/Event.js - ---------------------------------------------------- - Pointer Gestures - ---------------------------------------------------- - 1 : click, dblclick, dbltap - 1+ : tap, taphold, drag, swipe - 2+ : pinch, rotate - ---------------------------------------------------- - Gyroscope Gestures - ---------------------------------------------------- - * shake - ---------------------------------------------------- - Fixes issues with - ---------------------------------------------------- - * mousewheel-Firefox uses DOMMouseScroll and does not return wheelDelta. - * devicemotion-Fixes issue where event.acceleration is not returned. - ---------------------------------------------------- - Ideas for the future - ---------------------------------------------------- - * Keyboard, GamePad, and other input abstractions. - * Event batching - i.e. for every x fingers down a new gesture is created. - */ + ---------------------------------------------------- + Event.proxy : 0.4.3 : 2013/07/17 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - /* - Create a new pointer gesture instance. - */ +/* + Create a new pointer gesture instance. +*/ - root.pointerSetup = function(conf, self) { - /// Configure. - conf.doc = conf.target.ownerDocument || conf.target; // Associated document. - conf.minFingers = conf.minFingers || conf.fingers || 1; // Minimum required fingers. - conf.maxFingers = conf.maxFingers || conf.fingers || Infinity; // Maximum allowed fingers. - conf.position = conf.position || "relative"; // Determines what coordinate system points are returned. - delete conf.fingers; //- - /// Convenience data. - self = self || {}; - self.gesture = conf.gesture; - self.target = conf.target; - self.pointerType = Event.pointerType; - /// - if (Event.modifyEventListener && conf.fromOverwrite) - conf.listener = Event.createPointerEvent; - /// Convenience commands. - var fingers = 0; - var type = self.gesture.indexOf("pointer") === 0 && Event.modifyEventListener ? "pointer" : "mouse"; - self.listener = conf.listener; - self.proxy = function(listener) { - self.defaultListener = conf.listener; - conf.listener = listener; - listener(conf.event, self); - }; - self.remove = function() { - if (conf.onPointerDown) - Event.remove(conf.target, type + "down", conf.onPointerDown); - if (conf.onPointerMove) - Event.remove(conf.doc, type + "move", conf.onPointerMove); - if (conf.onPointerUp) - Event.remove(conf.doc, type + "up", conf.onPointerUp); - }; - self.resume = function(opt) { - if (conf.onPointerMove && (!opt || opt.move)) - Event.add(conf.doc, type + "move", conf.onPointerMove); - if (conf.onPointerUp && (!opt || opt.move)) - Event.add(conf.doc, type + "up", conf.onPointerUp); - conf.fingers = fingers; - }; - self.pause = function(opt) { - fingers = conf.fingers; - if (conf.onPointerMove && (!opt || opt.move)) - Event.remove(conf.doc, type + "move", conf.onPointerMove); - if (conf.onPointerUp && (!opt || opt.up)) - Event.remove(conf.doc, type + "up", conf.onPointerUp); - conf.fingers = 0; - }; - /// - return self; - }; +root.pointerSetup = function(conf, self) { + /// Configure. + conf.doc = conf.target.ownerDocument || conf.target; // Associated document. + conf.minFingers = conf.minFingers || conf.fingers || 1; // Minimum required fingers. + conf.maxFingers = conf.maxFingers || conf.fingers || Infinity; // Maximum allowed fingers. + conf.position = conf.position || "relative"; // Determines what coordinate system points are returned. + delete conf.fingers; //- + /// Convenience data. + self = self || {}; + self.enabled = true; + self.gesture = conf.gesture; + self.target = conf.target; + self.env = conf.env; + /// + if (Event.modifyEventListener && conf.fromOverwrite) { + conf.oldListener = conf.listener; + conf.listener = Event.createPointerEvent; + } + /// Convenience commands. + var fingers = 0; + var type = self.gesture.indexOf("pointer") === 0 && Event.modifyEventListener ? "pointer" : "mouse"; + if (conf.oldListener) self.oldListener = conf.oldListener; + self.listener = conf.listener; + self.proxy = function(listener) { + self.defaultListener = conf.listener; + conf.listener = listener; + listener(conf.event, self); + }; + self.add = function() { + if (self.enabled === true) return; + if (conf.onPointerDown) Event.add(conf.target, type + "down", conf.onPointerDown); + if (conf.onPointerMove) Event.add(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp) Event.add(conf.doc, type + "up", conf.onPointerUp); + self.enabled = true; + }; + self.remove = function() { + if (self.enabled === false) return; + if (conf.onPointerDown) Event.remove(conf.target, type + "down", conf.onPointerDown); + if (conf.onPointerMove) Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp) Event.remove(conf.doc, type + "up", conf.onPointerUp); + self.reset(); + self.enabled = false; + }; + self.pause = function(opt) { + if (conf.onPointerMove && (!opt || opt.move)) Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.up)) Event.remove(conf.doc, type + "up", conf.onPointerUp); + fingers = conf.fingers; + conf.fingers = 0; + }; + self.resume = function(opt) { + if (conf.onPointerMove && (!opt || opt.move)) Event.add(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.up)) Event.add(conf.doc, type + "up", conf.onPointerUp); + conf.fingers = fingers; + }; + self.reset = function() { + conf.tracker = {}; + conf.fingers = 0; + }; + /// + return self; +}; - /* - Begin proxied pointer command. - */ +/* + Begin proxied pointer command. +*/ - root.pointerStart = function(event, self, conf) { - var addTouchStart = function(touch, sid) { - var bbox = conf.bbox; - var pt = track[sid] = {}; - /// - switch (conf.position) { - case "absolute": // Absolute from within window. - pt.offsetX = 0; - pt.offsetY = 0; - break; - case "difference": // Relative from origin. - pt.offsetX = touch.pageX; - pt.offsetY = touch.pageY; - break; - case "move": // Move target element. - pt.offsetX = touch.pageX - bbox.x1; - pt.offsetY = touch.pageY - bbox.y1; - break; - default: // Relative from within target. - pt.offsetX = bbox.x1; - pt.offsetY = bbox.y1; - break; - } - /// - if (conf.position === "relative") { - var x = (touch.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; - var y = (touch.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; - } else { - var x = (touch.pageX - pt.offsetX); - var y = (touch.pageY - pt.offsetY); - } - /// - pt.rotation = 0; - pt.scale = 1; - pt.startTime = pt.moveTime = (new Date).getTime(); - pt.move = {x: x, y: y}; - pt.start = {x: x, y: y}; - /// - conf.fingers++; - }; - /// - conf.event = event; - if (self.defaultListener) { - conf.listener = self.defaultListener; - delete self.defaultListener; - } - /// - var isTouchStart = !conf.fingers; - var track = conf.tracker; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - // Adding touch events to tracking. - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; // Touch ID. - // Track the current state of the touches. - if (conf.fingers) { - if (conf.fingers >= conf.maxFingers) { - var ids = []; - for (var sid in conf.tracker) - ids.push(sid); - self.identifier = ids.join(","); - return isTouchStart; - } - var fingers = 0; // Finger ID. - for (var rid in track) { - // Replace removed finger. - if (track[rid].up) { - delete track[rid]; - addTouchStart(touch, sid); - conf.cancel = true; - break; - } - fingers++; - } - // Add additional finger. - if (track[sid]) - continue; - addTouchStart(touch, sid); - } else { // Start tracking fingers. - track = conf.tracker = {}; - self.bbox = conf.bbox = root.getBoundingBox(conf.target); - conf.fingers = 0; - conf.cancel = false; - addTouchStart(touch, sid); - } - } - /// - var ids = []; - for (var sid in conf.tracker) - ids.push(sid); - self.identifier = ids.join(","); - /// - return isTouchStart; - }; +var sp = Event.supports; +Event.pointerType = sp.mouse ? "mouse" : sp.touch ? "touch" : "mspointer"; +root.pointerStart = function(event, self, conf) { + var type = (event.type || "mousedown").toUpperCase(); + if (type.indexOf("MOUSE") === 0) Event.pointerType = "mouse"; + else if (type.indexOf("TOUCH") === 0) Event.pointerType = "touch"; + else if (type.indexOf("MSPOINTER") === 0) Event.pointerType = "mspointer"; + /// + var addTouchStart = function(touch, sid) { + var bbox = conf.bbox; + var pt = track[sid] = {}; + /// + switch(conf.position) { + case "absolute": // Absolute from within window. + pt.offsetX = 0; + pt.offsetY = 0; + break; + case "differenceFromLast": // Since last coordinate recorded. + pt.offsetX = touch.pageX; + pt.offsetY = touch.pageY; + break; + case "difference": // Relative from origin. + pt.offsetX = touch.pageX; + pt.offsetY = touch.pageY; + break; + case "move": // Move target element. + pt.offsetX = touch.pageX - bbox.x1; + pt.offsetY = touch.pageY - bbox.y1; + break; + default: // Relative from within target. + pt.offsetX = bbox.x1; + pt.offsetY = bbox.y1; + break; + } + /// + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - pt.offsetX); + var y = (touch.pageY + bbox.scrollTop - pt.offsetY); + } else { + var x = (touch.pageX - pt.offsetX); + var y = (touch.pageY - pt.offsetY); + } + /// + pt.rotation = 0; + pt.scale = 1; + pt.startTime = pt.moveTime = (new Date()).getTime(); + pt.move = { x: x, y: y }; + pt.start = { x: x, y: y }; + /// + conf.fingers ++; + }; + /// + conf.event = event; + if (self.defaultListener) { + conf.listener = self.defaultListener; + delete self.defaultListener; + } + /// + var isTouchStart = !conf.fingers; + var track = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Adding touch events to tracking. + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; // Touch ID. + // Track the current state of the touches. + if (conf.fingers) { + if (conf.fingers >= conf.maxFingers) { + var ids = []; + for (var sid in conf.tracker) ids.push(sid); + self.identifier = ids.join(","); + return isTouchStart; + } + var fingers = 0; // Finger ID. + for (var rid in track) { + // Replace removed finger. + if (track[rid].up) { + delete track[rid]; + addTouchStart(touch, sid); + conf.cancel = true; + break; + } + fingers ++; + } + // Add additional finger. + if (track[sid]) continue; + addTouchStart(touch, sid); + } else { // Start tracking fingers. + track = conf.tracker = {}; + self.bbox = conf.bbox = root.getBoundingBox(conf.target); + conf.fingers = 0; + conf.cancel = false; + addTouchStart(touch, sid); + } + } + /// + var ids = []; + for (var sid in conf.tracker) ids.push(sid); + self.identifier = ids.join(","); + /// + return isTouchStart; +}; - /* - End proxied pointer command. - */ +/* + End proxied pointer command. +*/ - root.pointerEnd = function(event, self, conf, onPointerUp) { - // Record changed touches have ended (iOS changedTouches is not reliable). - var touches = event.touches || []; - var length = touches.length; - var exists = {}; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier; - exists[sid || Infinity] = true; - } - for (var sid in conf.tracker) { - var track = conf.tracker[sid]; - if (exists[sid] || track.up) - continue; - if (onPointerUp) { // add changedTouches to mouse. - onPointerUp({ - pageX: track.pageX, - pageY: track.pageY, - changedTouches: [{ - pageX: track.pageX, - pageY: track.pageY, - identifier: sid === "Infinity" ? Infinity : sid - }] - }, "up"); - } - track.up = true; - conf.fingers--; - } - /* // This should work but fails in Safari on iOS4 so not using it. - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - // Record changed touches have ended (this should work). - for (var i = 0; i < length; i ++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; - var track = conf.tracker[sid]; - if (track && !track.up) { - if (onPointerUp) { // add changedTouches to mouse. - onPointerUp({ - changedTouches: [{ - pageX: track.pageX, - pageY: track.pageY, - identifier: sid === "Infinity" ? Infinity : sid - }] - }, "up"); - } - track.up = true; - conf.fingers --; - } - } */ - // Wait for all fingers to be released. - if (conf.fingers !== 0) - return false; - // Record total number of fingers gesture used. - var ids = []; - conf.gestureFingers = 0; - for (var sid in conf.tracker) { - conf.gestureFingers++; - ids.push(sid); - } - self.identifier = ids.join(","); - // Our pointer gesture has ended. - return true; - }; +root.pointerEnd = function(event, self, conf, onPointerUp) { + // Record changed touches have ended (iOS changedTouches is not reliable). + var touches = event.touches || []; + var length = touches.length; + var exists = {}; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier; + exists[sid || Infinity] = true; + } + for (var sid in conf.tracker) { + var track = conf.tracker[sid]; + if (exists[sid] || track.up) continue; + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + pageX: track.pageX, + pageY: track.pageY, + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers --; + } +/* // This should work but fails in Safari on iOS4 so not using it. + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Record changed touches have ended (this should work). + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var track = conf.tracker[sid]; + if (track && !track.up) { + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers --; + } + } */ + // Wait for all fingers to be released. + if (conf.fingers !== 0) return false; + // Record total number of fingers gesture used. + var ids = []; + conf.gestureFingers = 0; + for (var sid in conf.tracker) { + conf.gestureFingers ++; + ids.push(sid); + } + self.identifier = ids.join(","); + // Our pointer gesture has ended. + return true; +}; - /* - Returns mouse coords in an array to match event.*Touches - ------------------------------------------------------------ - var touch = event.changedTouches || root.getCoords(event); - */ +/* + Returns mouse coords in an array to match event.*Touches + ------------------------------------------------------------ + var touch = event.changedTouches || root.getCoords(event); +*/ - root.getCoords = function(event) { - if (typeof(event.pageX) !== "undefined") { // Desktop browsers. - root.getCoords = function(event) { - return Array({ - type: "mouse", - x: event.pageX, - y: event.pageY, - pageX: event.pageX, - pageY: event.pageY, - identifier: Infinity - }); - }; - } else { // Internet Explorer <= 8.0 - root.getCoords = function(event) { - event = event || window.event; - return Array({ - type: "mouse", - x: event.clientX + document.documentElement.scrollLeft, - y: event.clientY + document.documentElement.scrollTop, - pageX: event.clientX + document.documentElement.scrollLeft, - pageY: event.clientY + document.documentElement.scrollTop, - identifier: Infinity - }); - }; - } - return root.getCoords(event); - }; +root.getCoords = function(event) { + if (typeof(event.pageX) !== "undefined") { // Desktop browsers. + root.getCoords = function(event) { + return Array({ + type: "mouse", + x: event.pageX, + y: event.pageY, + pageX: event.pageX, + pageY: event.pageY, + identifier: event.pointerId || Infinity // pointerId is MS + }); + }; + } else { // Internet Explorer <= 8.0 + root.getCoords = function(event) { + event = event || window.event; + return Array({ + type: "mouse", + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop, + pageX: event.clientX + document.documentElement.scrollLeft, + pageY: event.clientY + document.documentElement.scrollTop, + identifier: Infinity + }); + }; + } + return root.getCoords(event); +}; - /* - Returns single coords in an object. - ------------------------------------------------------------ - var mouse = root.getCoord(event); - */ +/* + Returns single coords in an object. + ------------------------------------------------------------ + var mouse = root.getCoord(event); +*/ - root.getCoord = function(event) { - if ("ontouchstart" in window) { // Mobile browsers. - var pX = 0; - var pY = 0; - root.getCoord = function(event) { - var touches = event.changedTouches; - if (touches.length) { // ontouchstart + ontouchmove - return { - x: pX = touches[0].pageX, - y: pY = touches[0].pageY - }; - } else { // ontouchend - return { - x: pX, - y: pY - }; - } - }; - } else if (typeof(event.pageX) !== "undefined" && typeof(event.pageY) !== "undefined") { // Desktop browsers. - root.getCoord = function(event) { - return { - x: event.pageX, - y: event.pageY - }; - }; - } else { // Internet Explorer <=8.0 - root.getCoord = function(event) { - event = event || window.event; - return { - x: event.clientX + document.documentElement.scrollLeft, - y: event.clientY + document.documentElement.scrollTop - }; - }; - } - return root.getCoord(event); - }; +root.getCoord = function(event) { + if ("ontouchstart" in window) { // Mobile browsers. + var pX = 0; + var pY = 0; + root.getCoord = function(event) { + var touches = event.changedTouches; + if (touches && touches.length) { // ontouchstart + ontouchmove + return { + x: pX = touches[0].pageX, + y: pY = touches[0].pageY + }; + } else { // ontouchend + return { + x: pX, + y: pY + }; + } + }; + } else if(typeof(event.pageX) !== "undefined" && typeof(event.pageY) !== "undefined") { // Desktop browsers. + root.getCoord = function(event) { + return { + x: event.pageX, + y: event.pageY + }; + }; + } else { // Internet Explorer <=8.0 + root.getCoord = function(event) { + event = event || window.event; + return { + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop + }; + }; + } + return root.getCoord(event); +}; - /* - Get target scale and position in space. - */ +/* + Get target scale and position in space. +*/ - root.getBoundingBox = function(o) { - if (o === window || o === document) - o = document.body; - /// - var bbox = { - x1: 0, - y1: 0, - x2: 0, - y2: 0, - scrollLeft: 0, - scrollTop: 0 - }; - /// - if (o === document.body) { - bbox.height = window.innerHeight; - bbox.width = window.innerWidth; - } else { - bbox.height = o.offsetHeight; - bbox.width = o.offsetWidth; - } - /// Get the scale of the element. - bbox.scaleX = o.width / bbox.width || 1; - bbox.scaleY = o.height / bbox.height || 1; - /// Get the offset of element. - var tmp = o; - while (tmp !== null) { - bbox.x1 += tmp.offsetLeft; - bbox.y1 += tmp.offsetTop; - tmp = tmp.offsetParent; - } - ; - /// Get the scroll of container element. - var tmp = o.parentNode; - while (tmp !== null) { - if (tmp === document.body) - break; - if (tmp.scrollTop === undefined) - break; - bbox.scrollLeft += tmp.scrollLeft; - bbox.scrollTop += tmp.scrollTop; - tmp = tmp.parentNode; - } - ; - /// Record the extent of box. - bbox.x2 = bbox.x1 + bbox.width; - bbox.y2 = bbox.y1 + bbox.height; - /// - return bbox; - }; +root.getBoundingBox = function(o) { + if (o === window || o === document) o = document.body; + /// + var bbox = {}; + var bcr = o.getBoundingClientRect(); + bbox.width = bcr.width; + bbox.height = bcr.height; + bbox.x1 = bcr.left; + bbox.y1 = bcr.top; + bbox.x2 = bbox.x1 + bbox.width; + bbox.y2 = bbox.y1 + bbox.height; + bbox.scaleX = bcr.width / o.offsetWidth || 1; + bbox.scaleY = bcr.height / o.offsetHeight || 1; + bbox.scrollLeft = 0; + bbox.scrollTop = 0; - /* - Keep track of metaKey, the proper ctrlKey for users platform. - */ + /// Get the scroll of container element. + var tmp = o.parentNode; + while (tmp !== null) { + if (tmp === document.body) break; + if (tmp.scrollTop === undefined) break; + var style = window.getComputedStyle(tmp); + var position = style.getPropertyValue("position"); + if (position === "absolute") { + break; + } else if (position === "fixed") { + bbox.scrollTop -= tmp.parentNode.scrollTop; + break; + } else { + bbox.scrollLeft += tmp.scrollLeft; + bbox.scrollTop += tmp.scrollTop; + } + tmp = tmp.parentNode; + }; + /// + return bbox; +}; - (function() { - var agent = navigator.userAgent.toLowerCase(); - var mac = agent.indexOf("macintosh") !== -1; - if (mac && agent.indexOf("khtml") !== -1) { // chrome, safari. - var watch = {91: true, 93: true}; - } else if (mac && agent.indexOf("firefox") !== -1) { // mac firefox. - var watch = {224: true}; - } else { // windows, linux, or mac opera. - var watch = {17: true}; - } - root.isMetaKey = function(event) { - return !!watch[event.keyCode]; - }; - root.metaTracker = function(event) { - if (watch[event.keyCode]) { - root.metaKey = event.type === "keydown"; - } - }; - })(); +/* + Keep track of metaKey, the proper ctrlKey for users platform. +*/ - return root; +(function() { + var agent = navigator.userAgent.toLowerCase(); + var mac = agent.indexOf("macintosh") !== -1; + if (mac && agent.indexOf("khtml") !== -1) { // chrome, safari. + var watch = { 91: true, 93: true }; + } else if (mac && agent.indexOf("firefox") !== -1) { // mac firefox. + var watch = { 224: true }; + } else { // windows, linux, or mac opera. + var watch = { 17: true }; + } + root.metaTrackerReset = function() { + root.metaKey = false; + root.ctrlKey = false; + root.shiftKey = false; + root.altKey = false; + }; + root.metaTracker = function(event) { + var check = !!watch[event.keyCode]; + if (check) root.metaKey = event.type === "keydown"; + root.ctrlKey = event.ctrlKey; + root.shiftKey = event.shiftKey; + root.altKey = event.altKey; + return check; + }; +})(); + +return root; })(Event.proxy); /* - "Click" event proxy. - ---------------------------------------------------- - Event.add(window, "click", function(event, self) {}); - */ + ---------------------------------------------------- + "MutationObserver" event proxy. + ---------------------------------------------------- + Author: Selvakumar Arumugam (MIT LICENSE) + http://stackoverflow.com/questions/10868104/can-you-have-a-javascript-hook-trigger-after-a-dom-elements-style-object-change + ---------------------------------------------------- +*/ +if (typeof(Event) === "undefined") var Event = {}; -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +Event.MutationObserver = (function() { + var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; + var DOMAttrModifiedSupported = (function() { + var p = document.createElement("p"); + var flag = false; + var fn = function() { flag = true }; + if (p.addEventListener) { + p.addEventListener("DOMAttrModified", fn, false); + } else if (p.attachEvent) { + p.attachEvent("onDOMAttrModified", fn); + } else { + return false; + } + /// + p.setAttribute("id", "target"); + /// + return flag; + })(); + /// + return function(container, callback) { + if (MutationObserver) { + var options = { + subtree: false, + attributes: true + }; + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(e) { + callback.call(e.target, e.attributeName); + }); + }); + observer.observe(container, options) + } else if (DOMAttrModifiedSupported) { + Event.add(container, "DOMAttrModified", function(e) { + callback.call(container, e.attrName); + }); + } else if ("onpropertychange" in document.body) { + Event.add(container, "propertychange", function(e) { + callback.call(container, window.event.propertyName); + }); + } + } +})(); +/* + "Click" event proxy. + ---------------------------------------------------- + Event.add(window, "click", function(event, self) {}); +*/ -Event.proxy = (function(root) { - "use strict"; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; - root.click = function(conf) { - conf.maxFingers = conf.maxFingers || conf.fingers || 1; - // Setting up local variables. - var EVENT; - // Tracking the events. - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - }; - conf.onPointerMove = function(event) { - EVENT = event; - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - if (EVENT.cancelBubble && ++EVENT.bubble > 1) - return; - var pointers = EVENT.changedTouches || root.getCoords(EVENT); - var pointer = pointers[0]; - var bbox = conf.bbox; - var newbbox = root.getBoundingBox(conf.target); - if (conf.position === "relative") { - var ax = (pointer.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - var ay = (pointer.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - var ax = (pointer.pageX - bbox.x1); - var ay = (pointer.pageY - bbox.y1); - } - if (ax > 0 && ax < bbox.width && // Within target coordinates. - ay > 0 && ay < bbox.height && - bbox.scrollTop === newbbox.scrollTop) { - conf.listener(EVENT, self); - } - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - self.state = "click"; - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +Event.proxy = (function(root) { "use strict"; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.click = root.click; +root.click = function(conf) { + conf.gesture = conf.gesture || "click"; + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var EVENT; + // Tracking the events. + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function (event) { + EVENT = event; + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (EVENT.cancelBubble && ++ EVENT.bubble > 1) return; + var pointers = EVENT.changedTouches || root.getCoords(EVENT); + var pointer = pointers[0]; + var bbox = conf.bbox; + var newbbox = root.getBoundingBox(conf.target); + if (conf.position === "relative") { + var ax = (pointer.pageX + bbox.scrollLeft - bbox.x1); + var ay = (pointer.pageY + bbox.scrollTop - bbox.y1); + } else { + var ax = (pointer.pageX - bbox.x1); + var ay = (pointer.pageY - bbox.y1); + } + if (ax > 0 && ax < bbox.width && // Within target coordinates. + ay > 0 && ay < bbox.height && + bbox.scrollTop === newbbox.scrollTop) { + /// + for (var key in conf.tracker) break; //- should be modularized? in dblclick too + var point = conf.tracker[key]; + self.x = point.start.x; + self.y = point.start.y; + /// + conf.listener(EVENT, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "click"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - return root; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.click = root.click; + +return root; })(Event.proxy); /* - "Double-Click" aka "Double-Tap" event proxy. - ---------------------------------------------------- - Event.add(window, "dblclick", function(event, self) {}); - ---------------------------------------------------- - Touch an target twice for <= 700ms, with less than 25 pixel drift. - */ + "Double-Click" aka "Double-Tap" event proxy. + ---------------------------------------------------- + Event.add(window, "dblclick", function(event, self) {}); + ---------------------------------------------------- + Touch an target twice for <= 700ms, with less than 25 pixel drift. +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.dbltap = - root.dblclick = function(conf) { - conf.maxFingers = conf.maxFingers || conf.fingers || 1; - // Setting up local variables. - var delay = 700; // in milliseconds - var time0, time1, timeout; - var pointer0, pointer1; - // Tracking the events. - conf.onPointerDown = function(event) { - var pointers = event.changedTouches || root.getCoords(event); - if (time0 && !time1) { // Click #2 - pointer1 = pointers[0]; - time1 = (new Date).getTime() - time0; - } else { // Click #1 - pointer0 = pointers[0]; - time0 = (new Date).getTime(); - time1 = 0; - clearTimeout(timeout); - timeout = setTimeout(function() { - time0 = 0; - }, delay); - } - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - }; - conf.onPointerMove = function(event) { - if (time0 && !time1) { - var pointers = event.changedTouches || root.getCoords(event); - pointer1 = pointers[0]; - } - var bbox = conf.bbox; - if (conf.position === "relative") { - var ax = (pointer1.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - var ay = (pointer1.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - var ax = (pointer1.pageX - bbox.x1); - var ay = (pointer1.pageY - bbox.y1); - } - if (!(ax > 0 && ax < bbox.width && // Within target coordinates.. - ay > 0 && ay < bbox.height && - Math.abs(pointer1.pageX - pointer0.pageX) <= 25 && // Within drift deviance. - Math.abs(pointer1.pageY - pointer0.pageY) <= 25)) { - // Cancel out this listener. - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - clearTimeout(timeout); - time0 = time1 = 0; - } - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - } - if (time0 && time1) { - if (time1 <= delay && !(event.cancelBubble && ++event.bubble > 1)) { - self.state = conf.gesture; - conf.listener(event, self); - } - clearTimeout(timeout); - time0 = time1 = 0; - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - self.state = "dblclick"; - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +root.dbltap = +root.dblclick = function(conf) { + conf.gesture = conf.gesture || "dbltap"; + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var delay = 700; // in milliseconds + var time0, time1, timeout; + var pointer0, pointer1; + // Tracking the events. + conf.onPointerDown = function (event) { + var pointers = event.changedTouches || root.getCoords(event); + if (time0 && !time1) { // Click #2 + pointer1 = pointers[0]; + time1 = (new Date()).getTime() - time0; + } else { // Click #1 + pointer0 = pointers[0]; + time0 = (new Date()).getTime(); + time1 = 0; + clearTimeout(timeout); + timeout = setTimeout(function() { + time0 = 0; + }, delay); + } + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function (event) { + if (time0 && !time1) { + var pointers = event.changedTouches || root.getCoords(event); + pointer1 = pointers[0]; + } + var bbox = conf.bbox; + if (conf.position === "relative") { + var ax = (pointer1.pageX + bbox.scrollLeft - bbox.x1); + var ay = (pointer1.pageY + bbox.scrollTop - bbox.y1); + } else { + var ax = (pointer1.pageX - bbox.x1); + var ay = (pointer1.pageY - bbox.y1); + } + if (!(ax > 0 && ax < bbox.width && // Within target coordinates.. + ay > 0 && ay < bbox.height && + Math.abs(pointer1.pageX - pointer0.pageX) <= 25 && // Within drift deviance. + Math.abs(pointer1.pageY - pointer0.pageY) <= 25)) { + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + if (time0 && time1) { + if (time1 <= delay && !(event.cancelBubble && ++event.bubble > 1)) { + self.state = conf.gesture; + for (var key in conf.tracker) break; + var point = conf.tracker[key]; + self.x = point.start.x; + self.y = point.start.y; + conf.listener(event, self); + } + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "dblclick"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.dbltap = root.dbltap; - Event.Gesture._gestureHandlers.dblclick = root.dblclick; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.dbltap = root.dbltap; +Event.Gesture._gestureHandlers.dblclick = root.dblclick; - return root; +return root; })(Event.proxy); /* - "Drag" event proxy (1+ fingers). - ---------------------------------------------------- - CONFIGURE: maxFingers, position. - ---------------------------------------------------- - Event.add(window, "drag", function(event, self) { - console.log(self.gesture, self.state, self.start, self.x, self.y, self.bbox); - }); - */ + "Drag" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: maxFingers, position. + ---------------------------------------------------- + Event.add(window, "drag", function(event, self) { + console.log(self.gesture, self.state, self.start, self.x, self.y, self.bbox); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.dragElement = function(that, event) { - root.drag({ - event: event, - target: that, - position: "move", - listener: function(event, self) { - that.style.left = self.x + "px"; - that.style.top = self.y + "px"; - Event.prevent(event); - } - }); - }; +root.dragElement = function(that, event) { + root.drag({ + event: event, + target: that, + position: "move", + listener: function(event, self) { + that.style.left = self.x + "px"; + that.style.top = self.y + "px"; + Event.prevent(event); + } + }); +}; - root.drag = function(conf) { - conf.gesture = "drag"; - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - if (!conf.monitor) { - Event.add(conf.doc, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - } - // Process event listener. - conf.onPointerMove(event, "down"); - }; - conf.onPointerMove = function(event, state) { - if (!conf.tracker) - return conf.onPointerDown(event); - var bbox = conf.bbox; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var identifier = touch.identifier || Infinity; - var pt = conf.tracker[identifier]; - // Identifier defined outside of listener. - if (!pt) - continue; - pt.pageX = touch.pageX; - pt.pageY = touch.pageY; - // Record data. - self.state = state || "move"; - self.identifier = identifier; - self.start = pt.start; - self.fingers = conf.fingers; - if (conf.position === "relative") { - self.x = (pt.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; - self.y = (pt.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; - } else { - self.x = (pt.pageX - pt.offsetX); - self.y = (pt.pageY - pt.offsetY); - } - /// - conf.listener(event, self); - } - }; - conf.onPointerUp = function(event) { - // Remove tracking for touch. - if (root.pointerEnd(event, self, conf, conf.onPointerMove)) { - if (!conf.monitor) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - } - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - if (conf.event) { - conf.onPointerDown(conf.event); - } else { // - Event.add(conf.target, "mousedown", conf.onPointerDown); - if (conf.monitor) { - Event.add(conf.doc, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - } - // Return this object. - return self; - }; +root.drag = function(conf) { + conf.gesture = "drag"; + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + if (!conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Process event listener. + conf.onPointerMove(event, "down"); + }; + conf.onPointerMove = function (event, state) { + if (!conf.tracker) return conf.onPointerDown(event); + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + // Identifier defined outside of listener. + if (!pt) continue; + pt.pageX = touch.pageX; + pt.pageY = touch.pageY; + // Record data. + self.state = state || "move"; + self.identifier = identifier; + self.start = pt.start; + self.fingers = conf.fingers; + if (conf.position === "differenceFromLast") { + self.x = (pt.pageX - pt.offsetX); + self.y = (pt.pageY - pt.offsetY); + pt.offsetX = pt.pageX; + pt.offsetY = pt.pageY; + } else if (conf.position === "relative") { + self.x = (pt.pageX + bbox.scrollLeft - pt.offsetX); + self.y = (pt.pageY + bbox.scrollTop - pt.offsetY); + } else { + self.x = (pt.pageX - pt.offsetX); + self.y = (pt.pageY - pt.offsetY); + } + /// + conf.listener(event, self); + } + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + if (root.pointerEnd(event, self, conf, conf.onPointerMove)) { + if (!conf.monitor) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + if (conf.event) { + conf.onPointerDown(conf.event); + } else { // + Event.add(conf.target, "mousedown", conf.onPointerDown); + if (conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.drag = root.drag; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.drag = root.drag; - return root; +return root; })(Event.proxy); /* - "Gesture" event proxy (2+ fingers). - ---------------------------------------------------- - CONFIGURE: minFingers, maxFingers. - ---------------------------------------------------- - Event.add(window, "gesture", function(event, self) { - console.log(self.rotation, self.scale, self.fingers, self.state); - }); - */ + "Gesture" event proxy (2+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - var RAD_DEG = Math.PI / 180; +var RAD_DEG = Math.PI / 180; - root.gesture = function(conf) { - conf.minFingers = conf.minFingers || conf.fingers || 2; - // Tracking the events. - conf.onPointerDown = function(event) { - var fingers = conf.fingers; - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - // Record gesture start. - if (conf.fingers === conf.minFingers && fingers !== conf.fingers) { - self.fingers = conf.minFingers; - self.scale = 1; - self.rotation = 0; - self.state = "start"; - var sids = ""; //- FIXME(mud): can generate duplicate IDs. - for (var key in conf.tracker) - sids += key; - self.identifier = parseInt(sids); - conf.listener(event, self); - } - }; - /// - conf.onPointerMove = function(event, state) { - var bbox = conf.bbox; - var points = conf.tracker; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - // Update tracker coordinates. - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; - var pt = points[sid]; - // Check whether "pt" is used by another gesture. - if (!pt) - continue; - // Find the actual coordinates. - if (conf.position === "relative") { - pt.move.x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - pt.move.y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - pt.move.x = (touch.pageX - bbox.x1); - pt.move.y = (touch.pageY - bbox.y1); - } - } - /// - if (conf.fingers < conf.minFingers) - return; - /// - var touches = []; - var scale = 0; - var rotation = 0; - /// Calculate centroid of gesture. - var centroidx = 0; - var centroidy = 0; - var length = 0; - for (var sid in points) { - var touch = points[sid]; - if (touch.up) - continue; - centroidx += touch.move.x; - centroidy += touch.move.y; - length++; - } - centroidx /= length; - centroidy /= length; - /// - for (var sid in points) { - var touch = points[sid]; - if (touch.up) - continue; - var start = touch.start; - if (!start.distance) { - var dx = start.x - centroidx; - var dy = start.y - centroidy; - start.distance = Math.sqrt(dx * dx + dy * dy); - start.angle = Math.atan2(dx, dy) / RAD_DEG; - } - // Calculate scale. - var dx = touch.move.x - centroidx; - var dy = touch.move.y - centroidy; - var distance = Math.sqrt(dx * dx + dy * dy); - scale += distance / start.distance; - // Calculate rotation. - var angle = Math.atan2(dx, dy) / RAD_DEG; - var rotate = (start.angle - angle + 360) % 360 - 180; - touch.DEG2 = touch.DEG1; // Previous degree. - touch.DEG1 = rotate > 0 ? rotate : -rotate; // Current degree. - if (typeof(touch.DEG2) !== "undefined") { - if (rotate > 0) { - touch.rotation += touch.DEG1 - touch.DEG2; - } else { - touch.rotation -= touch.DEG1 - touch.DEG2; - } - rotation += touch.rotation; - } - // Attach current points to self. - touches.push(touch.move); - } - /// - self.touches = touches; - self.fingers = conf.fingers; - self.scale = scale / conf.fingers; - self.rotation = rotation / conf.fingers; - self.state = "change"; - conf.listener(event, self); - }; - conf.onPointerUp = function(event) { - // Remove tracking for touch. - var fingers = conf.fingers; - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - } - // Check whether fingers has dropped below minFingers. - if (fingers === conf.minFingers && conf.fingers < conf.minFingers) { - self.fingers = conf.fingers; - self.state = "end"; - conf.listener(event, self); - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +root.gesture = function(conf) { + conf.gesture = conf.gesture || "gesture"; + conf.minFingers = conf.minFingers || conf.fingers || 2; + // Tracking the events. + conf.onPointerDown = function (event) { + var fingers = conf.fingers; + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + // Record gesture start. + if (conf.fingers === conf.minFingers && fingers !== conf.fingers) { + self.fingers = conf.minFingers; + self.scale = 1; + self.rotation = 0; + self.state = "start"; + var sids = ""; //- FIXME(mud): can generate duplicate IDs. + for (var key in conf.tracker) sids += key; + self.identifier = parseInt(sids); + conf.listener(event, self); + } + }; + /// + conf.onPointerMove = function (event, state) { + var bbox = conf.bbox; + var points = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Update tracker coordinates. + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var pt = points[sid]; + // Check whether "pt" is used by another gesture. + if (!pt) continue; + // Find the actual coordinates. + if (conf.position === "relative") { + pt.move.x = (touch.pageX + bbox.scrollLeft - bbox.x1); + pt.move.y = (touch.pageY + bbox.scrollTop - bbox.y1); + } else { + pt.move.x = (touch.pageX - bbox.x1); + pt.move.y = (touch.pageY - bbox.y1); + } + } + /// + if (conf.fingers < conf.minFingers) return; + /// + var touches = []; + var scale = 0; + var rotation = 0; + /// Calculate centroid of gesture. + var centroidx = 0; + var centroidy = 0; + var length = 0; + for (var sid in points) { + var touch = points[sid]; + if (touch.up) continue; + centroidx += touch.move.x; + centroidy += touch.move.y; + length ++; + } + centroidx /= length; + centroidy /= length; + /// + for (var sid in points) { + var touch = points[sid]; + if (touch.up) continue; + var start = touch.start; + if (!start.distance) { + var dx = start.x - centroidx; + var dy = start.y - centroidy; + start.distance = Math.sqrt(dx * dx + dy * dy); + start.angle = Math.atan2(dx, dy) / RAD_DEG; + } + // Calculate scale. + var dx = touch.move.x - centroidx; + var dy = touch.move.y - centroidy; + var distance = Math.sqrt(dx * dx + dy * dy); + scale += distance / start.distance; + // Calculate rotation. + var angle = Math.atan2(dx, dy) / RAD_DEG; + var rotate = (start.angle - angle + 360) % 360 - 180; + touch.DEG2 = touch.DEG1; // Previous degree. + touch.DEG1 = rotate > 0 ? rotate : -rotate; // Current degree. + if (typeof(touch.DEG2) !== "undefined") { + if (rotate > 0) { + touch.rotation += touch.DEG1 - touch.DEG2; + } else { + touch.rotation -= touch.DEG1 - touch.DEG2; + } + rotation += touch.rotation; + } + // Attach current points to self. + touches.push(touch.move); + } + /// + self.touches = touches; + self.fingers = conf.fingers; + self.scale = scale / conf.fingers; + self.rotation = rotation / conf.fingers; + self.state = "change"; + conf.listener(event, self); + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + var fingers = conf.fingers; + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + // Check whether fingers has dropped below minFingers. + if (fingers === conf.minFingers && conf.fingers < conf.minFingers) { + self.fingers = conf.fingers; + self.state = "end"; + conf.listener(event, self); + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.gesture = root.gesture; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.gesture = root.gesture; - return root; +return root; })(Event.proxy); /* - "Pointer" event proxy (1+ fingers). - ---------------------------------------------------- - CONFIGURE: minFingers, maxFingers. - ---------------------------------------------------- - Event.add(window, "gesture", function(event, self) { - console.log(self.rotation, self.scale, self.fingers, self.state); - }); - */ + "Pointer" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.pointerdown = - root.pointermove = - root.pointerup = function(conf) { - if (conf.target.isPointerEmitter) - return; - // Tracking the events. - var isDown = true; - conf.onPointerDown = function(event) { - isDown = false; - self.gesture = "pointerdown"; - conf.listener(event, self); - }; - conf.onPointerMove = function(event) { - self.gesture = "pointermove"; - conf.listener(event, self, isDown); - }; - conf.onPointerUp = function(event) { - isDown = true; - self.gesture = "pointerup"; - conf.listener(event, self, true); - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - Event.add(conf.target, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - // Return this object. - conf.target.isPointerEmitter = true; - return self; - }; +root.pointerdown = +root.pointermove = +root.pointerup = function(conf) { + conf.gesture = conf.gesture || "pointer"; + if (conf.target.isPointerEmitter) return; + // Tracking the events. + var isDown = true; + conf.onPointerDown = function (event) { + isDown = false; + self.gesture = "pointerdown"; + conf.listener(event, self); + }; + conf.onPointerMove = function (event) { + self.gesture = "pointermove"; + conf.listener(event, self, isDown); + }; + conf.onPointerUp = function (event) { + isDown = true; + self.gesture = "pointerup"; + conf.listener(event, self, true); + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + Event.add(conf.target, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Return this object. + conf.target.isPointerEmitter = true; + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.pointerdown = root.pointerdown; - Event.Gesture._gestureHandlers.pointermove = root.pointermove; - Event.Gesture._gestureHandlers.pointerup = root.pointerup; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.pointerdown = root.pointerdown; +Event.Gesture._gestureHandlers.pointermove = root.pointermove; +Event.Gesture._gestureHandlers.pointerup = root.pointerup; - return root; +return root; })(Event.proxy); /* - "Device Motion" and "Shake" event proxy. - ---------------------------------------------------- - http://developer.android.com/reference/android/hardware/SensorEvent.html#values - ---------------------------------------------------- - Event.add(window, "shake", function(event, self) {}); - Event.add(window, "devicemotion", function(event, self) { - console.log(self.acceleration, self.accelerationIncludingGravity); - }); - */ + "Device Motion" and "Shake" event proxy. + ---------------------------------------------------- + http://developer.android.com/reference/android/hardware/SensorEvent.html#values + ---------------------------------------------------- + Event.add(window, "shake", function(event, self) {}); + Event.add(window, "devicemotion", function(event, self) { + console.log(self.acceleration, self.accelerationIncludingGravity); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.shake = function(conf) { - // Externally accessible data. - var self = { - gesture: "devicemotion", - acceleration: {}, - accelerationIncludingGravity: {}, - target: conf.target, - listener: conf.listener, - remove: function() { - window.removeEventListener('devicemotion', onDeviceMotion, false); - } - }; - // Setting up local variables. - var threshold = 4; // Gravitational threshold. - var timeout = 1000; // Timeout between shake events. - var timeframe = 200; // Time between shakes. - var shakes = 3; // Minimum shakes to trigger event. - var lastShake = (new Date).getTime(); - var gravity = {x: 0, y: 0, z: 0}; - var delta = { - x: {count: 0, value: 0}, - y: {count: 0, value: 0}, - z: {count: 0, value: 0} - }; - // Tracking the events. - var onDeviceMotion = function(e) { - var alpha = 0.8; // Low pass filter. - var o = e.accelerationIncludingGravity; - gravity.x = alpha * gravity.x + (1 - alpha) * o.x; - gravity.y = alpha * gravity.y + (1 - alpha) * o.y; - gravity.z = alpha * gravity.z + (1 - alpha) * o.z; - self.accelerationIncludingGravity = gravity; - self.acceleration.x = o.x - gravity.x; - self.acceleration.y = o.y - gravity.y; - self.acceleration.z = o.z - gravity.z; - /// - if (conf.gesture === "devicemotion") { - conf.listener(e, self); - return; - } - var data = "xyz"; - var now = (new Date).getTime(); - for (var n = 0, length = data.length; n < length; n++) { - var letter = data[n]; - var ACCELERATION = self.acceleration[letter]; - var DELTA = delta[letter]; - var abs = Math.abs(ACCELERATION); - /// Check whether another shake event was recently registered. - if (now - lastShake < timeout) - continue; - /// Check whether delta surpasses threshold. - if (abs > threshold) { - var idx = now * ACCELERATION / abs; - var span = Math.abs(idx + DELTA.value); - // Check whether last delta was registered within timeframe. - if (DELTA.value && span < timeframe) { - DELTA.value = idx; - DELTA.count++; - // Check whether delta count has enough shakes. - if (DELTA.count === shakes) { - conf.listener(e, self); - // Reset tracking. - lastShake = now; - DELTA.value = 0; - DELTA.count = 0; - } - } else { - // Track first shake. - DELTA.value = idx; - DELTA.count = 1; - } - } - } - }; - // Attach events. - if (!window.addEventListener) - return; - window.addEventListener('devicemotion', onDeviceMotion, false); - // Return this object. - return self; - }; +root.shake = function(conf) { + // Externally accessible data. + var self = { + gesture: "devicemotion", + acceleration: {}, + accelerationIncludingGravity: {}, + target: conf.target, + listener: conf.listener, + remove: function() { + window.removeEventListener('devicemotion', onDeviceMotion, false); + } + }; + // Setting up local variables. + var threshold = 4; // Gravitational threshold. + var timeout = 1000; // Timeout between shake events. + var timeframe = 200; // Time between shakes. + var shakes = 3; // Minimum shakes to trigger event. + var lastShake = (new Date()).getTime(); + var gravity = { x: 0, y: 0, z: 0 }; + var delta = { + x: { count: 0, value: 0 }, + y: { count: 0, value: 0 }, + z: { count: 0, value: 0 } + }; + // Tracking the events. + var onDeviceMotion = function(e) { + var alpha = 0.8; // Low pass filter. + var o = e.accelerationIncludingGravity; + gravity.x = alpha * gravity.x + (1 - alpha) * o.x; + gravity.y = alpha * gravity.y + (1 - alpha) * o.y; + gravity.z = alpha * gravity.z + (1 - alpha) * o.z; + self.accelerationIncludingGravity = gravity; + self.acceleration.x = o.x - gravity.x; + self.acceleration.y = o.y - gravity.y; + self.acceleration.z = o.z - gravity.z; + /// + if (conf.gesture === "devicemotion") { + conf.listener(e, self); + return; + } + var data = "xyz"; + var now = (new Date()).getTime(); + for (var n = 0, length = data.length; n < length; n ++) { + var letter = data[n]; + var ACCELERATION = self.acceleration[letter]; + var DELTA = delta[letter]; + var abs = Math.abs(ACCELERATION); + /// Check whether another shake event was recently registered. + if (now - lastShake < timeout) continue; + /// Check whether delta surpasses threshold. + if (abs > threshold) { + var idx = now * ACCELERATION / abs; + var span = Math.abs(idx + DELTA.value); + // Check whether last delta was registered within timeframe. + if (DELTA.value && span < timeframe) { + DELTA.value = idx; + DELTA.count ++; + // Check whether delta count has enough shakes. + if (DELTA.count === shakes) { + conf.listener(e, self); + // Reset tracking. + lastShake = now; + DELTA.value = 0; + DELTA.count = 0; + } + } else { + // Track first shake. + DELTA.value = idx; + DELTA.count = 1; + } + } + } + }; + // Attach events. + if (!window.addEventListener) return; + window.addEventListener('devicemotion', onDeviceMotion, false); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.shake = root.shake; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.shake = root.shake; - return root; +return root; })(Event.proxy); /* - "Swipe" event proxy (1+ fingers). - ---------------------------------------------------- - CONFIGURE: snap, threshold, maxFingers. - ---------------------------------------------------- - Event.add(window, "swipe", function(event, self) { - console.log(self.velocity, self.angle); - }); - */ + "Swipe" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: snap, threshold, maxFingers. + ---------------------------------------------------- + Event.add(window, "swipe", function(event, self) { + console.log(self.velocity, self.angle); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - var RAD_DEG = Math.PI / 180; +var RAD_DEG = Math.PI / 180; - root.swipe = function(conf) { - conf.snap = conf.snap || 90; // angle snap. - conf.threshold = conf.threshold || 1; // velocity threshold. - // Tracking the events. - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - }; - conf.onPointerMove = function(event) { - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; - var o = conf.tracker[sid]; - // Identifier defined outside of listener. - if (!o) - continue; - o.move.x = touch.pageX; - o.move.y = touch.pageY; - o.moveTime = (new Date).getTime(); - } - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - /// - var velocity1; - var velocity2 - var degree1; - var degree2; - /// Calculate centroid of gesture. - var start = {x: 0, y: 0}; - var endx = 0; - var endy = 0; - var length = 0; - /// - for (var sid in conf.tracker) { - var touch = conf.tracker[sid]; - var xdist = touch.move.x - touch.start.x; - var ydist = touch.move.y - touch.start.y; +root.swipe = function(conf) { + conf.snap = conf.snap || 90; // angle snap. + conf.threshold = conf.threshold || 1; // velocity threshold. + conf.gesture = conf.gesture || "swipe"; + // Tracking the events. + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function (event) { + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var o = conf.tracker[sid]; + // Identifier defined outside of listener. + if (!o) continue; + o.move.x = touch.pageX; + o.move.y = touch.pageY; + o.moveTime = (new Date()).getTime(); + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + /// + var velocity1; + var velocity2 + var degree1; + var degree2; + /// Calculate centroid of gesture. + var start = { x: 0, y: 0 }; + var endx = 0; + var endy = 0; + var length = 0; + /// + for (var sid in conf.tracker) { + var touch = conf.tracker[sid]; + var xdist = touch.move.x - touch.start.x; + var ydist = touch.move.y - touch.start.y; + /// + endx += touch.move.x; + endy += touch.move.y; + start.x += touch.start.x; + start.y += touch.start.y; + length ++; + /// + var distance = Math.sqrt(xdist * xdist + ydist * ydist); + var ms = touch.moveTime - touch.startTime; + var degree2 = Math.atan2(xdist, ydist) / RAD_DEG + 180; + var velocity2 = ms ? distance / ms : 0; + if (typeof(degree1) === "undefined") { + degree1 = degree2; + velocity1 = velocity2; + } else if (Math.abs(degree2 - degree1) <= 20) { + degree1 = (degree1 + degree2) / 2; + velocity1 = (velocity1 + velocity2) / 2; + } else { + return; + } + } + /// + var fingers = conf.gestureFingers; + if (conf.minFingers <= fingers && conf.maxFingers >= fingers) { + if (velocity1 > conf.threshold) { + start.x /= length; + start.y /= length; + self.start = start; + self.x = endx / length; + self.y = endy / length; + self.angle = -((((degree1 / conf.snap + 0.5) >> 0) * conf.snap || 360) - 360); + self.velocity = velocity1; + self.fingers = fingers; + self.state = "swipe"; + conf.listener(event, self); + } + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - endx += touch.move.x; - endy += touch.move.y; - start.x += touch.start.x; - start.y += touch.start.y; - length++; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.swipe = root.swipe; - - var distance = Math.sqrt(xdist * xdist + ydist * ydist); - var ms = touch.moveTime - touch.startTime; - var degree2 = Math.atan2(xdist, ydist) / RAD_DEG + 180; - var velocity2 = ms ? distance / ms : 0; - if (typeof(degree1) === "undefined") { - degree1 = degree2; - velocity1 = velocity2; - } else if (Math.abs(degree2 - degree1) <= 20) { - degree1 = (degree1 + degree2) / 2; - velocity1 = (velocity1 + velocity2) / 2; - } else { - return; - } - } - /// - if (velocity1 > conf.threshold) { - start.x /= length; - start.y /= length; - self.start = start; - self.x = endx / length; - self.y = endy / length; - self.angle = -((((degree1 / conf.snap + 0.5) >> 0) * conf.snap || 360) - 360); - self.velocity = velocity1; - self.fingers = conf.gestureFingers; - self.state = "swipe"; - conf.listener(event, self); - } - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; - - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.swipe = root.swipe; - - return root; +return root; })(Event.proxy); /* - "Tap" and "Longpress" event proxy. - ---------------------------------------------------- - CONFIGURE: delay (longpress), timeout (tap). - ---------------------------------------------------- - Event.add(window, "tap", function(event, self) { - console.log(self.fingers); - }); - ---------------------------------------------------- - multi-finger tap // touch an target for <= 250ms. - multi-finger longpress // touch an target for >= 500ms - */ + "Tap" and "Longpress" event proxy. + ---------------------------------------------------- + CONFIGURE: delay (longpress), timeout (tap). + ---------------------------------------------------- + Event.add(window, "tap", function(event, self) { + console.log(self.fingers); + }); + ---------------------------------------------------- + multi-finger tap // touch an target for <= 250ms. + multi-finger longpress // touch an target for >= 500ms +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.tap = - root.longpress = function(conf) { - conf.delay = conf.delay || 500; - conf.timeout = conf.timeout || 250; - // Setting up local variables. - var timestamp, timeout; - // Tracking the events. - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - timestamp = (new Date).getTime(); - // Initialize event listeners. - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - // Make sure this is a "longpress" event. - if (conf.gesture !== "longpress") - return; - timeout = setTimeout(function() { - if (event.cancelBubble && ++event.bubble > 1) - return; - // Make sure no fingers have been changed. - var fingers = 0; - for (var key in conf.tracker) { - if (conf.tracker[key].end === true) - return; - if (conf.cancel) - return; - fingers++; - } - // Send callback. - self.state = "start"; - self.fingers = fingers; - conf.listener(event, self); - }, conf.delay); - } - }; - conf.onPointerMove = function(event) { - var bbox = conf.bbox; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var identifier = touch.identifier || Infinity; - var pt = conf.tracker[identifier]; - if (!pt) - continue; - if (conf.position === "relative") { - var x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - var y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - var x = (touch.pageX - bbox.x1); - var y = (touch.pageY - bbox.y1); - } - if (!(x > 0 && x < bbox.width && // Within target coordinates.. - y > 0 && y < bbox.height && - Math.abs(x - pt.start.x) <= 25 && // Within drift deviance. - Math.abs(y - pt.start.y) <= 25)) { - // Cancel out this listener. - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - conf.cancel = true; - return; - } - } - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - clearTimeout(timeout); - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - if (event.cancelBubble && ++event.bubble > 1) - return; - // Callback release on longpress. - if (conf.gesture === "longpress") { - if (self.state === "start") { - self.state = "end"; - conf.listener(event, self); - } - return; - } - // Cancel event due to movement. - if (conf.cancel) - return; - // Ensure delay is within margins. - if ((new Date).getTime() - timestamp > conf.timeout) - return; - // Send callback. - self.state = "tap"; - self.fingers = conf.gestureFingers; - conf.listener(event, self); - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +root.longpress = function(conf) { + conf.gesture = "longpress"; + return root.tap(conf); +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.tap = root.tap; - Event.Gesture._gestureHandlers.longpress = root.longpress; +root.tap = function(conf) { + conf.delay = conf.delay || 500; + conf.timeout = conf.timeout || 250; + conf.driftDeviance = conf.driftDeviance || 10; + conf.gesture = conf.gesture || "tap"; + // Setting up local variables. + var timestamp, timeout; + // Tracking the events. + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + timestamp = (new Date()).getTime(); + // Initialize event listeners. + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Make sure this is a "longpress" event. + if (conf.gesture !== "longpress") return; + timeout = setTimeout(function() { + if (event.cancelBubble && ++event.bubble > 1) return; + // Make sure no fingers have been changed. + var fingers = 0; + for (var key in conf.tracker) { + var point = conf.tracker[key]; + if (point.end === true) return; + if (conf.cancel) return; + fingers ++; + } + // Send callback. + if (conf.minFingers <= fingers && conf.maxFingers >= fingers) { + self.state = "start"; + self.fingers = fingers; + self.x = point.start.x; + self.y = point.start.y; + conf.listener(event, self); + } + }, conf.delay); + } + }; + conf.onPointerMove = function (event) { + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + if (!pt) continue; + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - bbox.x1); + var y = (touch.pageY + bbox.scrollTop - bbox.y1); + } else { + var x = (touch.pageX - bbox.x1); + var y = (touch.pageY - bbox.y1); + } + /// + var dx = x - pt.start.x; + var dy = y - pt.start.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (!(x > 0 && x < bbox.width && // Within target coordinates.. + y > 0 && y < bbox.height && + distance <= conf.driftDeviance)) { // Within drift deviance. + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + conf.cancel = true; + return; + } + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + clearTimeout(timeout); + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (event.cancelBubble && ++event.bubble > 1) return; + // Callback release on longpress. + if (conf.gesture === "longpress") { + if (self.state === "start") { + self.state = "end"; + conf.listener(event, self); + } + return; + } + // Cancel event due to movement. + if (conf.cancel) return; + // Ensure delay is within margins. + if ((new Date()).getTime() - timestamp > conf.timeout) return; + // Send callback. + var fingers = conf.gestureFingers; + if (conf.minFingers <= fingers && conf.maxFingers >= fingers) { + self.state = "tap"; + self.fingers = conf.gestureFingers; + conf.listener(event, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - return root; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.tap = root.tap; +Event.Gesture._gestureHandlers.longpress = root.longpress; + +return root; })(Event.proxy); /* - "Mouse Wheel" event proxy. - ---------------------------------------------------- - Event.add(window, "wheel", function(event, self) { - console.log(self.state, self.wheelDelta); - }); - */ + "Mouse Wheel" event proxy. + ---------------------------------------------------- + Event.add(window, "wheel", function(event, self) { + console.log(self.state, self.wheelDelta); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.wheel = function(conf) { - // Configure event listener. - var interval; - var timeout = conf.timeout || 150; - var count = 0; - // Externally accessible data. - var self = { - gesture: "wheel", - state: "start", - wheelDelta: 0, - target: conf.target, - listener: conf.listener, - remove: function() { - conf.target[remove](type, onMouseWheel, false); - } - }; - // Tracking the events. - var onMouseWheel = function(event) { - event = event || window.event; - self.state = count++ ? "change" : "start"; - self.wheelDelta = event.detail ? event.detail * -20 : event.wheelDelta; - conf.listener(event, self); - clearTimeout(interval); - interval = setTimeout(function() { - count = 0; - self.state = "end"; - self.wheelDelta = 0; - conf.listener(event, self); - }, timeout); - }; - // Attach events. - var add = document.addEventListener ? "addEventListener" : "attachEvent"; - var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; - var type = Event.supports("mousewheel") ? "mousewheel" : "DOMMouseScroll"; - conf.target[add](type, onMouseWheel, false); - // Return this object. - return self; - }; +root.wheel = function(conf) { + // Configure event listener. + var interval; + var timeout = conf.timeout || 150; + var count = 0; + // Externally accessible data. + var self = { + gesture: "wheel", + state: "start", + wheelDelta: 0, + target: conf.target, + listener: conf.listener, + preventElasticBounce: function() { + var target = this.target; + var scrollTop = target.scrollTop; + var top = scrollTop + target.offsetHeight; + var height = target.scrollHeight; + if (top === height && this.wheelDelta <= 0) Event.cancel(event); + else if (scrollTop === 0 && this.wheelDelta >= 0) Event.cancel(event); + Event.stop(event); + }, + add: function() { + conf.target[add](type, onMouseWheel, false); + }, + remove: function() { + conf.target[remove](type, onMouseWheel, false); + } + }; + // Tracking the events. + var onMouseWheel = function(event) { + event = event || window.event; + self.state = count++ ? "change" : "start"; + self.wheelDelta = event.detail ? event.detail * -20 : event.wheelDelta; + conf.listener(event, self); + clearTimeout(interval); + interval = setTimeout(function() { + count = 0; + self.state = "end"; + self.wheelDelta = 0; + conf.listener(event, self); + }, timeout); + }; + // Attach events. + var add = document.addEventListener ? "addEventListener" : "attachEvent"; + var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; + var type = Event.getEventSupport("mousewheel") ? "mousewheel" : "DOMMouseScroll"; + conf.target[add](type, onMouseWheel, false); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.wheel = root.wheel; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.wheel = root.wheel; - return root; +return root; })(Event.proxy); +/* + "Orientation Change" + ---------------------------------------------------- + https://developer.apple.com/library/safari/documentation/SafariDOMAdditions/Reference/DeviceOrientationEventClassRef/DeviceOrientationEvent/DeviceOrientationEvent.html#//apple_ref/doc/uid/TP40010526 + ---------------------------------------------------- + Event.add(window, "deviceorientation", function(event, self) {}); +*/ + +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; + +Event.proxy = (function(root) { "use strict"; + +root.orientation = function(conf) { + // Externally accessible data. + var self = { + gesture: "orientationchange", + previous: null, /* Report the previous orientation */ + current: window.orientation, + target: conf.target, + listener: conf.listener, + remove: function() { + window.removeEventListener('orientationchange', onOrientationChange, false); + } + }; + + // Tracking the events. + var onOrientationChange = function(e) { + + self.previous = self.current; + self.current = window.orientation; + if(self.previous !== null && self.previous != self.current) { + conf.listener(e, self); + return; + } -/** - * Wrapper around `console.log` (when available) - * @param {Any} values Values to log - */ -fabric.log = function() { }; + }; + // Attach events. + if (window.DeviceOrientationEvent) { + window.addEventListener("orientationchange", onOrientationChange, false); + } + // Return this object. + return self; +}; -/** - * Wrapper around `console.warn` (when available) - * @param {Any} Values to log as a warning - */ -fabric.warn = function() { }; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.orientation = root.orientation; -if (typeof console !== 'undefined') { - if (typeof console.log !== 'undefined' && console.log.apply) { - fabric.log = function() { - return console.log.apply(console, arguments); - }; - } - if (typeof console.warn !== 'undefined' && console.warn.apply) { - fabric.warn = function() { - return console.warn.apply(console, arguments); - }; - } -} +return root; + +})(Event.proxy); (function(){ @@ -3729,7 +3645,8 @@ if (typeof console !== 'undefined') { } /** - * Stops event observing for a particular event handler + * Stops event observing for a particular event handler. Calling this method + * without arguments removes all handlers for all events * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead) * @memberOf fabric.Observable * @alias off @@ -3741,8 +3658,12 @@ if (typeof console !== 'undefined') { function stopObserving(eventName, handler) { if (!this.__eventListeners) return; + // remove all key/value pairs (event name -> event handler) + if (arguments.length === 0) { + this.__eventListeners = { }; + } // one object with key/value pairs was passed - if (arguments.length === 1 && typeof arguments[0] === 'object') { + else if (arguments.length === 1 && typeof arguments[0] === 'object') { for (var prop in eventName) { _removeEventListener.call(this, prop, eventName[prop]); } @@ -3873,6 +3794,21 @@ fabric.Collection = { return this; }, + /** + * Returns an array of children objects of this instance + * Type parameter introduced in 1.3.10 + * @param {String} [type] When specified, only objects of this type are returned + * @return {Array} + */ + getObjects: function(type) { + if (typeof type === 'undefined') { + return this._objects; + } + return this._objects.filter(function(o) { + return o.type === type; + }); + }, + /** * Returns object at specified index * @param {Number} index @@ -3923,464 +3859,546 @@ fabric.Collection = { (function(global) { var sqrt = Math.sqrt, - atan2 = Math.atan2; + atan2 = Math.atan2, + PiBy180 = Math.PI / 180; /** * @namespace fabric.util */ - fabric.util = { }; + fabric.util = { - /** - * Removes value from an array. - * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` - * @static - * @memberOf fabric.util - * @param {Array} array - * @param {Any} value - * @return {Array} original array - */ - function removeFromArray(array, value) { - var idx = array.indexOf(value); - if (idx !== -1) { - array.splice(idx, 1); - } - return array; - } + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @param {Array} array + * @param {Any} value + * @return {Array} original array + */ + removeFromArray: function(array, value) { + var idx = array.indexOf(value); + if (idx !== -1) { + array.splice(idx, 1); + } + return array; + }, - /** - * Returns random number between 2 specified ones. - * @static - * @memberOf fabric.util - * @param {Number} min lower limit - * @param {Number} max upper limit - * @return {Number} random value (between min and max) - */ - function getRandomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; - } + /** + * Returns random number between 2 specified ones. + * @static + * @memberOf fabric.util + * @param {Number} min lower limit + * @param {Number} max upper limit + * @return {Number} random value (between min and max) + */ + getRandomInt: function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, - var PiBy180 = Math.PI / 180; + /** + * Transforms degrees to radians. + * @static + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + degreesToRadians: function(degrees) { + return degrees * PiBy180; + }, - /** - * Transforms degrees to radians. - * @static - * @memberOf fabric.util - * @param {Number} degrees value in degrees - * @return {Number} value in radians - */ - function degreesToRadians(degrees) { - return degrees * PiBy180; - } + /** + * Transforms radians to degrees. + * @static + * @memberOf fabric.util + * @param {Number} radians value in radians + * @return {Number} value in degrees + */ + radiansToDegrees: function(radians) { + return radians / PiBy180; + }, - /** - * Transforms radians to degrees. - * @static - * @memberOf fabric.util - * @param {Number} radians value in radians - * @return {Number} value in degrees - */ - function radiansToDegrees(radians) { - return radians / PiBy180; - } + /** + * Rotates `point` around `origin` with `radians` + * @static + * @memberOf fabric.util + * @param {fabric.Point} The point to rotate + * @param {fabric.Point} The origin of the rotation + * @param {Number} The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + var sin = Math.sin(radians), + cos = Math.cos(radians); - /** - * Rotates `point` around `origin` with `radians` - * @static - * @memberOf fabric.util - * @param {fabric.Point} The point to rotate - * @param {fabric.Point} The origin of the rotation - * @param {Number} The radians of the angle for the rotation - * @return {fabric.Point} The new rotated point - */ - function rotatePoint(point, origin, radians) { - var sin = Math.sin(radians), - cos = Math.cos(radians); + point.subtractEquals(origin); - point.subtractEquals(origin); + var rx = point.x * cos - point.y * sin, + ry = point.x * sin + point.y * cos; - var rx = point.x * cos - point.y * sin; - var ry = point.x * sin + point.y * cos; + return new fabric.Point(rx, ry).addEquals(origin); + }, - return new fabric.Point(rx, ry).addEquals(origin); - } - - /** - * Apply transform t to point p - * @static - * @memberOf fabric.util - * @param {fabric.Point} p The point to transform - * @param {Array} t The transform - * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied - * @return {fabric.Point} The transformed point - */ - function transformPoint(p, t, ignoreOffset) { - if (ignoreOffset) { + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[1] * p.y, + t[2] * p.x + t[3] * p.y + ); + } return new fabric.Point( - t[0] * p.x + t[1] * p.y, - t[2] * p.x + t[3] * p.y + t[0] * p.x + t[1] * p.y + t[4], + t[2] * p.x + t[3] * p.y + t[5] ); - } - return new fabric.Point( - t[0] * p.x + t[1] * p.y + t[4], - t[2] * p.x + t[3] * p.y + t[5] - ); - } - - /** - * Invert transformation t - * @static - * @memberOf fabric.util - * @param {Array} t The transform - * @return {Array} The inverted transform - */ - function invertTransform(t) { - var r = t.slice(), - a = 1 / (t[0] * t[3] - t[1] * t[2]); - r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; - var o = transformPoint({x: t[4], y: t[5]}, r); - r[4] = -o.x; - r[5] = -o.y; - return r - } + }, - /** - * A wrapper around Number#toFixed, which contrary to native method returns number, not string. - * @static - * @memberOf fabric.util - * @param {Number | String} number number to operate on - * @param {Number} fractionDigits number of fraction digits to "leave" - * @return {Number} - */ - function toFixed(number, fractionDigits) { - return parseFloat(Number(number).toFixed(fractionDigits)); - } + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var r = t.slice(), + a = 1 / (t[0] * t[3] - t[1] * t[2]); + r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; + var o = fabric.util.transformPoint({x: t[4], y: t[5]}, r); + r[4] = -o.x; + r[5] = -o.y; + return r + }, - /** - * Function which always returns `false`. - * @static - * @memberOf fabric.util - * @return {Boolean} - */ - function falseFunction() { - return false; - } + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @memberOf fabric.util + * @param {Number | String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, - /** - * Returns klass "Class" object of given namespace - * @memberOf fabric.util - * @param {String} type Type of object (eg. 'circle') - * @param {String} namespace Namespace to get klass "Class" object from - * @return {Object} klass "Class" - */ - function getKlass(type, namespace) { - // capitalize first letter only - type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); - return resolveNamespace(namespace)[type]; - } + /** + * Function which always returns `false`. + * @static + * @memberOf fabric.util + * @return {Boolean} + */ + falseFunction: function() { + return false; + }, - /** - * Returns object of given namespace - * @memberOf fabric.util - * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' - * @return {Object} Object for given namespace (default fabric) - */ - function resolveNamespace(namespace) { - if (!namespace) return fabric; + /** + * Returns klass "Class" object of given namespace + * @memberOf fabric.util + * @param {String} type Type of object (eg. 'circle') + * @param {String} namespace Namespace to get klass "Class" object from + * @return {Object} klass "Class" + */ + getKlass: function(type, namespace) { + // capitalize first letter only + type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); + return fabric.util.resolveNamespace(namespace)[type]; + }, - var parts = namespace.split('.'), - len = parts.length, - obj = global || fabric.window; + /** + * Returns object of given namespace + * @memberOf fabric.util + * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' + * @return {Object} Object for given namespace (default fabric) + */ + resolveNamespace: function(namespace) { + if (!namespace) return fabric; - for (var i = 0; i < len; ++i) { - obj = obj[parts[i]]; - } + var parts = namespace.split('.'), + len = parts.length, + obj = global || fabric.window; - return obj; - } + for (var i = 0; i < len; ++i) { + obj = obj[parts[i]]; + } + + return obj; + }, + + /** + * Loads image element from given url and passes it to a callback + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {Any} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } - /** - * Loads image element from given url and passes it to a callback - * @memberOf fabric.util - * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {Any} context optional Context to invoke callback in - */ - function loadImage(url, callback, context) { - if (url) { var img = fabric.util.createImage(); + /** @ignore */ img.onload = function () { callback && callback.call(context, img); - img = img.onload = null; + img = img.onload = img.onerror = null; }; + + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + if (url.indexOf('data') !== 0) { + img.crossOrigin = crossOrigin || ''; + } + img.src = url; - } - else { - callback && callback.call(context, url); - } - } + }, - /** - * Creates corresponding fabric instances from their object representations - * @static - * @memberOf fabric.util - * @param {Array} objects Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * @param {Function} [reviver] Method for further parsing of object elements, called after each fabric object created. - */ - function enlivenObjects(objects, callback, namespace, reviver) { + /** + * Creates corresponding fabric instances from their object representations + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {Function} [reviver] Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || [ ]; - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - if (callback) { - callback(enlivenedObjects); + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects); } } - } - var enlivenedObjects = [ ], - numLoadedObjects = 0, - numTotalObjects = objects.length; + var enlivenedObjects = [ ], + numLoadedObjects = 0, + numTotalObjects = objects.length; - objects.forEach(function (o, index) { - // if sparse array - if (!o || !o.type) { - onLoaded(); + if (!numTotalObjects) { + callback && callback(enlivenedObjects); return; } - var klass = fabric.util.getKlass(o.type, namespace); - if (klass.async) { - klass.fromObject(o, function (obj, error) { - if (!error) { - enlivenedObjects[index] = obj; - reviver && reviver(o, enlivenedObjects[index]); - } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { onLoaded(); - }); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + if (klass.async) { + klass.fromObject(o, function (obj, error) { + if (!error) { + enlivenedObjects[index] = obj; + reviver && reviver(o, enlivenedObjects[index]); + } + onLoaded(); + }); + } + else { + enlivenedObjects[index] = klass.fromObject(o); + reviver && reviver(o, enlivenedObjects[index]); + onLoaded(); + } + }); + }, + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @return {fabric.Object|fabric.PathGroup} + */ + groupSVGElements: function(elements, options, path) { + var object; + + if (elements.length > 1) { + object = new fabric.PathGroup(elements, options); } else { - enlivenedObjects[index] = klass.fromObject(o); - reviver && reviver(o, enlivenedObjects[index]); - onLoaded(); + object = elements[0]; } - }); - } - /** - * Groups SVG elements (usually those retrieved from SVG document) - * @static - * @memberOf fabric.util - * @param {Array} elements SVG elements to group - * @param {Object} [options] Options object - * @return {fabric.Object|fabric.PathGroup} - */ - function groupSVGElements(elements, options, path) { - var object; + if (typeof path !== 'undefined') { + object.setSourcePath(path); + } + return object; + }, - if (elements.length > 1) { - object = new fabric.PathGroup(elements, options); - } - else { - object = elements[0]; - } - - if (typeof path !== 'undefined') { - object.setSourcePath(path); - } - return object; - } - - /** - * Populates an object with properties of another object - * @static - * @memberOf fabric.util - * @param {Object} source Source object - * @param {Object} destination Destination object - * @return {Array} properties Propertie names to include - */ - function populateWithProperties(source, destination, properties) { - if (properties && Object.prototype.toString.call(properties) === '[object Array]') { - for (var i = 0, len = properties.length; i < len; i++) { - if (properties[i] in source) { - destination[properties[i]] = source[properties[i]]; + /** + * Populates an object with properties of another object + * @static + * @memberOf fabric.util + * @param {Object} source Source object + * @param {Object} destination Destination object + * @return {Array} properties Propertie names to include + */ + populateWithProperties: function(source, destination, properties) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + for (var i = 0, len = properties.length; i < len; i++) { + if (properties[i] in source) { + destination[properties[i]] = source[properties[i]]; + } } } - } - } + }, - /** - * Draws a dashed line between two points - * - * This method is used to draw dashed line around selection area. - * See dotted stroke in canvas - * - * @param ctx {Canvas} context - * @param x {Number} start x coordinate - * @param y {Number} start y coordinate - * @param x2 {Number} end x coordinate - * @param y2 {Number} end y coordinate - * @param da {Array} dash array pattern - */ - function drawDashedLine(ctx, x, y, x2, y2, da) { - var dx = x2 - x, - dy = y2 - y, - len = sqrt(dx*dx + dy*dy), - rot = atan2(dy, dx), - dc = da.length, - di = 0, - draw = true; + /** + * Draws a dashed line between two points + * + * This method is used to draw dashed line around selection area. + * See dotted stroke in canvas + * + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x start x coordinate + * @param {Number} y start y coordinate + * @param {Number} x2 end x coordinate + * @param {Number} y2 end y coordinate + * @param {Array} da dash array pattern + */ + drawDashedLine: function(ctx, x, y, x2, y2, da) { + var dx = x2 - x, + dy = y2 - y, + len = sqrt(dx*dx + dy*dy), + rot = atan2(dy, dx), + dc = da.length, + di = 0, + draw = true; - ctx.save(); - ctx.translate(x, y); - ctx.moveTo(0, 0); - ctx.rotate(rot); + ctx.save(); + ctx.translate(x, y); + ctx.moveTo(0, 0); + ctx.rotate(rot); - x = 0; - while (len > x) { - x += da[di++ % dc]; - if (x > len) { - x = len; - } - ctx[draw ? 'lineTo' : 'moveTo'](x, 0); - draw = !draw; - } - - ctx.restore(); - } - - /** - * Creates canvas element and initializes it via excanvas if necessary - * @static - * @memberOf fabric.util - * @param {CanvasElement} [canvasEl] optional canvas element to initialize; when not given, element is created implicitly - * @return {CanvasElement} initialized canvas element - */ - function createCanvasElement(canvasEl) { - canvasEl || (canvasEl = fabric.document.createElement('canvas')); - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - return canvasEl; - } - - /** - * Creates image element (works on client and node) - * @static - * @memberOf fabric.util - * @return {HTMLImageElement} HTML image element - */ - function createImage() { - return fabric.isLikelyNode - ? new (require('canvas').Image)() - : fabric.document.createElement('img'); - } - - /** - * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array - * @static - * @memberOf fabric.util - * @param {Object} klass "Class" to create accessors for - */ - function createAccessors(klass) { - var proto = klass.prototype; - - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - } - - /** - * @static - * @memberOf fabric.util - * @param {fabric.Object} receiver Object implementing `clipTo` method - * @param {CanvasRenderingContext2D} ctx Context to clip - */ - function clipContext(receiver, ctx) { - ctx.save(); - ctx.beginPath(); - receiver.clipTo(ctx); - ctx.clip(); - } - - /** - * Multiply matrix A by matrix B to nest transformations - * @static - * @memberOf fabric.util - * @param {Array} matrixA First transformMatrix - * @param {Array} matrixB Second transformMatrix - * @return {Array} The product of the two transform matrices - */ - function multiplyTransformMatrices(matrixA, matrixB) { - // Matrix multiply matrixA * matrixB - var a = [ - [matrixA[0], matrixA[2], matrixA[4]], - [matrixA[1], matrixA[3], matrixA[5]], - [0 , 0 , 1 ] - ]; - - var b = [ - [matrixB[0], matrixB[2], matrixB[4]], - [matrixB[1], matrixB[3], matrixB[5]], - [0 , 0 , 1 ] - ]; - - var result = []; - for (var r=0; r<3; r++) { - result[r] = []; - for (var c=0; c<3; c++) { - var sum = 0; - for (var k=0; k<3; k++) { - sum += a[r][k]*b[k][c]; + x = 0; + while (len > x) { + x += da[di++ % dc]; + if (x > len) { + x = len; } - - result[r][c] = sum; + ctx[draw ? 'lineTo' : 'moveTo'](x, 0); + draw = !draw; } + + ctx.restore(); + }, + + /** + * Creates canvas element and initializes it via excanvas if necessary + * @static + * @memberOf fabric.util + * @param {CanvasElement} [canvasEl] optional canvas element to initialize; + * when not given, element is created implicitly + * @return {CanvasElement} initialized canvas element + */ + createCanvasElement: function(canvasEl) { + canvasEl || (canvasEl = fabric.document.createElement('canvas')); + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + return canvasEl; + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + createImage: function() { + return fabric.isLikelyNode + ? new (require('canvas').Image)() + : fabric.document.createElement('img'); + }, + + /** + * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array + * @static + * @memberOf fabric.util + * @param {Object} klass "Class" to create accessors for + */ + createAccessors: function(klass) { + var proto = klass.prototype; + + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + }, + + /** + * @static + * @memberOf fabric.util + * @param {fabric.Object} receiver Object implementing `clipTo` method + * @param {CanvasRenderingContext2D} ctx Context to clip + */ + clipContext: function(receiver, ctx) { + ctx.save(); + ctx.beginPath(); + receiver.clipTo(ctx); + ctx.clip(); + }, + + /** + * Multiply matrix A by matrix B to nest transformations + * @static + * @memberOf fabric.util + * @param {Array} matrixA First transformMatrix + * @param {Array} matrixB Second transformMatrix + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(matrixA, matrixB) { + // Matrix multiply matrixA * matrixB + var a = [ + [matrixA[0], matrixA[2], matrixA[4]], + [matrixA[1], matrixA[3], matrixA[5]], + [0 , 0 , 1 ] + ]; + + var b = [ + [matrixB[0], matrixB[2], matrixB[4]], + [matrixB[1], matrixB[3], matrixB[5]], + [0 , 0 , 1 ] + ]; + + var result = []; + for (var r=0; r<3; r++) { + result[r] = []; + for (var c=0; c<3; c++) { + var sum = 0; + for (var k=0; k<3; k++) { + sum += a[r][k]*b[k][c]; + } + + result[r][c] = sum; + } + } + + return [ + result[0][0], + result[1][0], + result[0][1], + result[1][1], + result[0][2], + result[1][2] + ]; + }, + + /** + * Returns string representation of function body + * @param {Function} fn Function to get body of + * @return {String} Function body + */ + getFunctionBody: function(fn) { + return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; + }, + + /** + * Normalizes polygon/polyline points according to their dimensions + * @param {Array} points + * @param {Object} options + */ + normalizePoints: function(points, options) { + var minX = fabric.util.array.min(points, 'x'), + minY = fabric.util.array.min(points, 'y'); + + minX = minX < 0 ? minX : 0; + minY = minX < 0 ? minY : 0; + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box + // (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2 + minX) || 0; + points[i].y -= (options.height / 2 + minY) || 0; + } + }, + + /** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Number} tolerance Tolerance + */ + isTransparent: function(ctx, x, y, tolerance) { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + var _isTransparent = true; + var imageData = ctx.getImageData( + x, y, (tolerance * 2) || 1, (tolerance * 2) || 1); + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (var i = 3, l = imageData.data.length; i < l; i += 4) { + var temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) break; // Stop if colour found + } + + imageData = null; + + return _isTransparent; } + }; - return [ - result[0][0], - result[1][0], - result[0][1], - result[1][1], - result[0][2], - result[1][2] - ]; - } +})(typeof exports !== 'undefined' ? exports : this); - function getFunctionBody(fn) { - return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; - } - function drawArc(ctx, x, y, coords) { - var rx = coords[0]; - var ry = coords[1]; - var rot = coords[2]; - var large = coords[3]; - var sweep = coords[4]; - var ex = coords[5]; - var ey = coords[6]; - var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); - for (var i=0; i 0 && sweep === 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i 0 && sweep === 0) { - th_arc -= 2 * Math.PI; - } - - var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); - var result = []; - for (var i=0; i= result) { - result = array[i][byProperty]; - } - } - } - else { - while (i--) { - if (array[i] >= result) { - result = array[i]; - } - } - } - return result; + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); } /** @@ -4727,21 +4745,29 @@ fabric.Collection = { * @return {Any} */ function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function find(array, byProperty, condition) { if (!array || array.length === 0) return undefined; var i = array.length - 1, result = byProperty ? array[i][byProperty] : array[i]; - if (byProperty) { while (i--) { - if (array[i][byProperty] < result) { + if (condition(array[i][byProperty], result)) { result = array[i][byProperty]; } } } else { while (i--) { - if (array[i] < result) { + if (condition(array[i], result)) { result = array[i]; } } @@ -4829,10 +4855,14 @@ function camelize(string) { * Capitalizes a string * @memberOf fabric.util.string * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. * @return {String} Capitalized version of a string */ -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); } /** @@ -5000,6 +5030,8 @@ fabric.util.string = { (function () { + var unknown = 'unknown'; + /* EVENT HANDLING */ function areHostMethods(object) { @@ -5174,41 +5206,14 @@ fabric.util.string = { function getPointer(event, upperCanvasEl) { event || (event = fabric.window.event); - var element = event.target || (typeof event.srcElement !== 'unknown' ? event.srcElement : null), - body = fabric.document.body || {scrollLeft: 0, scrollTop: 0}, - docElement = fabric.document.documentElement, - orgElement = element, - scrollLeft = 0, - scrollTop = 0, - firstFixedAncestor; + var element = event.target || + (typeof event.srcElement !== unknown ? event.srcElement : null); - while (element && element.parentNode && !firstFixedAncestor) { - element = element.parentNode; - - if (element !== fabric.document && - fabric.util.getElementStyle(element, 'position') === 'fixed') { - firstFixedAncestor = element; - } - - if (element !== fabric.document && - orgElement !== upperCanvasEl && - fabric.util.getElementStyle(element, 'position') === 'absolute') { - scrollLeft = 0; - scrollTop = 0; - } - else if (element === fabric.document) { - scrollLeft = body.scrollLeft || docElement.scrollLeft || 0; - scrollTop = body.scrollTop || docElement.scrollTop || 0; - } - else { - scrollLeft += element.scrollLeft || 0; - scrollTop += element.scrollTop || 0; - } - } + var scroll = fabric.util.getScrollLeftTop(element, upperCanvasEl); return { - x: pointerX(event) + scrollLeft, - y: pointerY(event) + scrollTop + x: pointerX(event) + scroll.left, + y: pointerY(event) + scroll.top }; } @@ -5216,29 +5221,28 @@ fabric.util.string = { // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] // need to investigate later - return (typeof event.clientX !== 'unknown' ? event.clientX : 0); + return (typeof event.clientX !== unknown ? event.clientX : 0); }; var pointerY = function(event) { - return (typeof event.clientY !== 'unknown' ? event.clientY : 0); + return (typeof event.clientY !== unknown ? event.clientY : 0); }; + function _getPointer(event, pageProp, clientProp) { + var touchProp = event.type === 'touchend' ? 'changedTouches' : 'touches'; + + return (event[touchProp] && event[touchProp][0] + ? (event[touchProp][0][pageProp] - (event[touchProp][0][pageProp] - event[touchProp][0][clientProp])) + || event[clientProp] + : event[clientProp]); + } + if (fabric.isTouchSupported) { pointerX = function(event) { - if (event.type !== 'touchend') { - return (event.touches && event.touches[0] ? - (event.touches[0].pageX - (event.touches[0].pageX - event.touches[0].clientX)) || event.clientX : event.clientX); - } - return (event.changedTouches && event.changedTouches[0] - ? (event.changedTouches[0].pageX - (event.changedTouches[0].pageX - event.changedTouches[0].clientX)) || event.clientX : event.clientX); + return _getPointer(event, 'pageX', 'clientX'); }; pointerY = function(event) { - if (event.type !== 'touchend') { - return (event.touches && event.touches[0] - ? (event.touches[0].pageY - (event.touches[0].pageY - event.touches[0].clientY)) || event.clientY : event.clientY); - } - return (event.changedTouches && event.changedTouches[0] - ? (event.changedTouches[0].pageY - (event.changedTouches[0].pageY - event.changedTouches[0].clientY)) || event.clientY : event.clientY); + return _getPointer(event, 'pageY', 'clientY'); }; } @@ -5414,6 +5418,47 @@ fabric.util.string = { wrapper.appendChild(element); return wrapper; } + + function getScrollLeftTop(element, upperCanvasEl) { + + var firstFixedAncestor, + origElement, + left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + origElement = element; + + while (element && element.parentNode && !firstFixedAncestor) { + + element = element.parentNode; + + if (element !== fabric.document && + fabric.util.getElementStyle(element, 'position') === 'fixed') { + firstFixedAncestor = element; + } + + if (element !== fabric.document && + origElement !== upperCanvasEl && + fabric.util.getElementStyle(element, 'position') === 'absolute') { + left = 0; + top = 0; + } + else if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + } + + return { left: left, top: top }; + } /** * Returns offset for a given element @@ -5423,10 +5468,11 @@ fabric.util.string = { * @return {Object} Object with "left" and "top" properties */ function getElementOffset(element) { - var docElem, win, + var docElem, box = {left: 0, top: 0}, doc = element && element.ownerDocument, offset = {left: 0, top: 0}, + scrollLeftTop, offsetAttributes = { 'borderLeftWidth': 'left', 'borderTopWidth': 'top', @@ -5446,14 +5492,12 @@ fabric.util.string = { if ( typeof element.getBoundingClientRect !== "undefined" ) { box = element.getBoundingClientRect(); } - if(doc != null && doc === doc.window){ - win = doc; - } else { - win = doc.nodeType === 9 && (doc.defaultView || doc.parentWindow); - } + + scrollLeftTop = fabric.util.getScrollLeftTop(element, null); + return { - left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + offset.left, - top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0) + offset.top + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top }; } @@ -5572,6 +5616,7 @@ fabric.util.string = { fabric.util.makeElement = makeElement; fabric.util.addClass = addClass; fabric.util.wrapElement = wrapElement; + fabric.util.getScrollLeftTop = getScrollLeftTop; fabric.util.getElementOffset = getElementOffset; fabric.util.getElementStyle = getElementStyle; @@ -5651,6 +5696,29 @@ fabric.util.string = { })(); +/** + * Wrapper around `console.log` (when available) + * @param {Any} values Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @param {Any} Values to log as a warning + */ +fabric.warn = function() { }; + +if (typeof console !== 'undefined') { + ['log', 'warn'].forEach(function(methodName) { + if (typeof console[methodName] !== 'undefined' && console[methodName].apply) { + fabric[methodName] = function() { + return console[methodName].apply(console, arguments); + }; + } + }); +} + + (function() { /** @@ -5667,34 +5735,37 @@ fabric.util.string = { */ function animate(options) { - options || (options = { }); + requestAnimFrame(function(timestamp) { + options || (options = { }); - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || endValue - startValue; + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; - options.onStart && options.onStart(); + options.onStart && options.onStart(); + + (function tick(ticktime) { + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start); + if (abort()) { + options.onComplete && options.onComplete(); + return; + } + onChange(easing(currentTime, startValue, byValue, duration)); + if (time > finish) { + options.onComplete && options.onComplete(); + return; + } + requestAnimFrame(tick); + })(start); + }); - (function tick() { - time = +new Date(); - var currentTime = time > finish ? duration : (time - start); - if (abort()) { - options.onComplete && options.onComplete(); - return; - } - onChange(easing(currentTime, startValue, byValue, duration)); - if (time > finish) { - options.onComplete && options.onComplete(); - return; - } - requestAnimFrame(tick); - })(); } var _requestAnimFrame = fabric.window.requestAnimationFrame || @@ -5707,6 +5778,7 @@ fabric.util.string = { }; /** * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method * @memberOf fabric.util * @param {Function} callback Callback to invoke * @param {DOMElement} element optional Element to associate with animation @@ -5723,38 +5795,16 @@ fabric.util.string = { (function() { - /** - * Quadratic easing in - * @memberOf fabric.util.ease - */ - function easeInQuad(t, b, c, d) { - return c*(t/=d)*t + b; + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return { a: a, c: c, p: p, s: s }; } - /** - * Quadratic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuad(t, b, c, d) { - return -c *(t/=d)*(t-2) + b; - } - - /** - * Quadratic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuad(t, b, c, d) { - t /= (d/2); - if (t < 1) return c/2*t*t + b; - return -c/2 * ((--t)*(t-2) - 1) + b; - } - - /** - * Cubic easing in - * @memberOf fabric.util.ease - */ - function easeInCubic(t, b, c, d) { - return c*(t/=d)*t*t + b; + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); } /** @@ -5915,9 +5965,8 @@ fabric.util.string = { t /= d; if (t===1) return b+c; if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; } /** @@ -5930,9 +5979,8 @@ fabric.util.string = { t /= d; if (t===1) return b+c; if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + var opts = normalize(a, c, p, s); + return opts.a*Math.pow(2,-10*t) * Math.sin( (t*d-opts.s)*(2*Math.PI)/opts.p ) + opts.c + b; } /** @@ -5945,10 +5993,9 @@ fabric.util.string = { t /= d/2; if (t===2) return b+c; if (!p) p=d*(0.3*1.5); - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; + var opts = normalize(a, c, p, s); + if (t < 1) return -0.5 * elastic(opts, t, d) + b; + return opts.a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-opts.s)*(2*Math.PI)/opts.p )*0.5 + opts.c + b; } /** @@ -6019,10 +6066,41 @@ fabric.util.string = { * @namespace fabric.util.ease */ fabric.util.ease = { - easeInQuad: easeInQuad, - easeOutQuad: easeOutQuad, - easeInOutQuad: easeInOutQuad, - easeInCubic: easeInCubic, + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c*(t/=d)*t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d/2); + if (t < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c*(t/=d)*t*t + b; + }, + easeOutCubic: easeOutCubic, easeInOutCubic: easeInOutCubic, easeInQuart: easeInQuart, @@ -6070,13 +6148,6 @@ fabric.util.string = { toFixed = fabric.util.toFixed, multiplyTransformMatrices = fabric.util.multiplyTransformMatrices; - fabric.SHARED_ATTRIBUTES = [ - "transform", - "fill", "fill-opacity", "fill-rule", - "opacity", - "stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width" - ]; - var attributesMap = { 'fill-opacity': 'fillOpacity', 'fill-rule': 'fillRule', @@ -6161,55 +6232,13 @@ fabric.util.string = { return attributes; } - /** - * Returns an object of attributes' name/value, given element and an array of attribute names; - * Parses parent "g" nodes recursively upwards. - * @static - * @memberOf fabric - * @param {DOMElement} element Element to parse - * @param {Array} attributes Array of attributes to parse - * @return {Object} object containing parsed attributes' names/values - */ - function parseAttributes(element, attributes) { - - if (!element) { - return; - } - - var value, - parentAttributes = { }; - - // if there's a parent container (`g` node), parse its attributes recursively upwards - if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { - parentAttributes = fabric.parseAttributes(element.parentNode, attributes); - } - - var ownAttributes = attributes.reduce(function(memo, attr) { - value = element.getAttribute(attr); - if (value) { - attr = normalizeAttr(attr); - value = normalizeValue(attr, value, parentAttributes); - - memo[attr] = value; - } - return memo; - }, { }); - - // add values parsed from style, which take precedence over attributes - // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) - - ownAttributes = extend(ownAttributes, - extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); - return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); - } - /** * Parses "transform" attribute, returning an array of values * @static * @function * @memberOf fabric - * @param attributeValue {String} string containing attribute value - * @return {Array} array of 6 elements representing transformation matrix + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { @@ -6256,13 +6285,22 @@ fabric.util.string = { // == begin transform regexp number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)', + comma_wsp = '(?:\\s+,?\\s*|,\\s*)', skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', - rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))', - scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', - translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + ')' + + comma_wsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', matrix = '(?:(matrix)\\s*\\(\\s*' + '(' + number + ')' + comma_wsp + @@ -6348,49 +6386,6 @@ fabric.util.string = { }; })(); - /** - * Parses "points" attribute, returning an array of values - * @static - * @memberOf fabric - * @param points {String} points attribute string - * @return {Array} array of points - */ - function parsePointsAttribute(points) { - - // points attribute is required and must not be empty - if (!points) return null; - - points = points.trim(); - var asPairs = points.indexOf(',') > -1; - - points = points.split(/\s+/); - var parsedPoints = [ ], i, len; - - // points could look like "10,20 30,40" or "10 20 30 40" - if (asPairs) { - i = 0; - len = points.length; - for (; i < len; i++) { - var pair = points[i].split(','); - parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); - } - } - else { - i = 0; - len = points.length; - for (; i < len; i+=2) { - parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); - } - } - - // odd number of points is an error - if (parsedPoints.length % 2 !== 0) { - // return null; - } - - return parsedPoints; - } - function parseFontDeclaration(value, oStyle) { // TODO: support non-px font size @@ -6424,166 +6419,43 @@ fabric.util.string = { } /** - * Parses "style" attribute, retuning an object with values - * @static - * @memberOf fabric - * @param {SVGElement} element Element to parse - * @return {Object} Objects with values parsed from style attribute of an element + * @private */ - function parseStyleAttribute(element) { - var oStyle = { }, - style = element.getAttribute('style'), - attr, value; + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); - if (!style) return oStyle; + attr = normalizeAttr(pair[0].trim().toLowerCase()); + value = normalizeValue(attr, pair[1].trim()); - if (typeof style === 'string') { - style.replace(/;$/, '').split(';').forEach(function (chunk) { - var pair = chunk.split(':'); - - attr = normalizeAttr(pair[0].trim().toLowerCase()); - value = normalizeValue(attr, pair[1].trim()); - - if (attr === 'font') { - parseFontDeclaration(value, oStyle); - } - else { - oStyle[attr] = value; - } - }); - } - else { - for (var prop in style) { - if (typeof style[prop] === 'undefined') continue; - - attr = normalizeAttr(prop.toLowerCase()); - value = normalizeValue(attr, style[prop]); - - if (attr === 'font') { - parseFontDeclaration(value, oStyle); - } - else { - oStyle[attr] = value; - } - } - } - - return oStyle; - } - - function resolveGradients(instances) { - for (var i = instances.length; i--; ) { - var instanceFillValue = instances[i].get('fill'); - - if (/^url\(/.test(instanceFillValue)) { - - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - - if (fabric.gradientDefs[gradientId]) { - instances[i].set('fill', - fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); - } - } - } - } - - /** - * Transforms an array of svg elements to corresponding fabric.* instances - * @static - * @memberOf fabric - * @param {Array} elements Array of elements to parse - * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) - * @param {Object} [options] Options object - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function parseElements(elements, callback, options, reviver) { - var instances = new Array(elements.length), i = elements.length; - - function checkIfDone() { - if (--i === 0) { - instances = instances.filter(function(el) { - return el != null; - }); - resolveGradients(instances); - callback(instances); - } - } - - for (var index = 0, el, len = elements.length; index < len; index++) { - el = elements[index]; - var klass = fabric[capitalize(el.tagName)]; - if (klass && klass.fromElement) { - try { - if (klass.async) { - klass.fromElement(el, (function(index, el) { - return function(obj) { - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - }; - })(index, el), options); - } - else { - var obj = klass.fromElement(el, options); - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - } - } - catch(err) { - fabric.log(err); - } + if (attr === 'font') { + parseFontDeclaration(value, oStyle); } else { - checkIfDone(); + oStyle[attr] = value; } - } + }); } /** - * Returns CSS rules for a given SVG document - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} CSS rules of this document + * @private */ - function getCSSRules(doc) { - var styles = doc.getElementsByTagName('style'), - allRules = { }, - rules; + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') continue; - // very crude parsing of style contents - for (var i = 0, len = styles.length; i < len; i++) { - var styleContents = styles[0].textContent; + attr = normalizeAttr(prop.toLowerCase()); + value = normalizeValue(attr, style[prop]); - // remove comments - styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); - - rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); - rules = rules.map(function(rule) { return rule.trim(); }); - - rules.forEach(function(rule) { - var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); - rule = match[1]; - var declaration = match[2].trim(), - propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); - - if (!allRules[rule]) { - allRules[rule] = { }; - } - - for (var i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(/\s*:\s*/), - property = pair[0], - value = pair[1]; - - allRules[rule][property] = value; - } - }); + if (attr === 'font') { + parseFontDeclaration(value, oStyle); + } + else { + oStyle[attr] = value; + } } - - return allRules; } /** @@ -6696,7 +6568,7 @@ fabric.util.string = { }; fabric.gradientDefs = fabric.getGradientDefs(doc); - fabric.cssRules = getCSSRules(doc); + fabric.cssRules = fabric.getCSSRules(doc); // Precedence of rules: style > class > attribute @@ -6740,53 +6612,6 @@ fabric.util.string = { } }; - /** - * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) - * @memberof fabric - * @param {String} url - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromURL(url, callback, reviver) { - - url = url.replace(/^\n\s*/, '').trim(); - - svgCache.has(url, function (hasUrl) { - if (hasUrl) { - svgCache.get(url, function (value) { - var enlivedRecord = _enlivenCachedObject(value); - callback(enlivedRecord.objects, enlivedRecord.options); - }); - } - else { - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - } - }); - - function onComplete(r) { - - var xml = r.responseXML; - if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { - xml = new ActiveXObject('Microsoft.XMLDOM'); - xml.async = 'false'; - //IE chokes on DOCTYPE - xml.loadXML(r.responseText.replace(//i,'')); - } - if (!xml.documentElement) return; - - fabric.parseSVGDocument(xml.documentElement, function (results, options) { - svgCache.set(url, { - objects: fabric.util.array.invoke(results, 'toObject'), - options: options - }); - callback(results, options); - }, reviver); - } - } - /** * @private */ @@ -6803,139 +6628,435 @@ fabric.util.string = { } /** - * Takes string corresponding to an SVG document, and parses it into a set of fabric objects - * @memberof fabric - * @param {String} string - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromString(string, callback, reviver) { - string = string.trim(); - var doc; - if (typeof DOMParser !== 'undefined') { - var parser = new DOMParser(); - if (parser && parser.parseFromString) { - doc = parser.parseFromString(string, 'text/xml'); - } - } - else if (fabric.window.ActiveXObject) { - doc = new ActiveXObject('Microsoft.XMLDOM'); - doc.async = 'false'; - //IE chokes on DOCTYPE - doc.loadXML(string.replace(//i,'')); - } - - fabric.parseSVGDocument(doc.documentElement, function (results, options) { - callback(results, options); - }, reviver); - } - - /** - * Creates markup containing SVG font faces - * @param {Array} objects Array of fabric objects - * @return {String} + * @private */ - function createSVGFontFacesMarkup(objects) { - var markup = ''; - - for (var i = 0, len = objects.length; i < len; i++) { - if (objects[i].type !== 'text' || !objects[i].path) continue; - - markup += [ - '@font-face {', - 'font-family: ', objects[i].fontFamily, '; ', - 'src: url(\'', objects[i].path, '\')', - '}' - ].join(''); - } - - if (markup) { - markup = [ - '' - ].join(''); - } - - return markup; - } - - /** - * Creates markup containing SVG referenced elements like patterns, gradients etc. - * @param {fabric.Canvas} canvas instance of fabric.Canvas - * @return {String} - */ - function createSVGRefElementsMarkup(canvas) { - var markup = ''; - - if (canvas.backgroundColor && canvas.backgroundColor.source) { - markup = [ - '', '' - ].join(''); + ); } - - return markup; - } - - /** - * Parses an SVG document, returning all of the gradient declarations found in it - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element - */ - function getGradientDefs(doc) { - var linearGradientEls = doc.getElementsByTagName('linearGradient'), - radialGradientEls = doc.getElementsByTagName('radialGradient'), - el, i, - gradientDefs = { }; - - i = linearGradientEls.length; - for (; i--; ) { - el = linearGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - i = radialGradientEls.length; - for (; i--; ) { - el = radialGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - return gradientDefs; } extend(fabric, { - parseAttributes: parseAttributes, - parseElements: parseElements, - parseStyleAttribute: parseStyleAttribute, - parsePointsAttribute: parsePointsAttribute, - getCSSRules: getCSSRules, + /** + * Initializes gradients on instances, according to gradients parsed from a document + * @param {Array} instances + */ + resolveGradients: function(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); - loadSVGFromURL: loadSVGFromURL, - loadSVGFromString: loadSVGFromString, + if (!(/^url\(/).test(instanceFillValue)) continue; - createSVGFontFacesMarkup: createSVGFontFacesMarkup, - createSVGRefElementsMarkup: createSVGRefElementsMarkup, + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - getGradientDefs: getGradientDefs + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, i, + gradientDefs = { }; + + i = linearGradientEls.length; + for (; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + i = radialGradientEls.length; + for (; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }; + + // if there's a parent container (`g` node), parse its attributes recursively upwards + if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { + attr = normalizeAttr(attr); + value = normalizeValue(attr, value, parentAttributes); + + memo[attr] = value; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + ownAttributes = extend(ownAttributes, + extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + + return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver) { + fabric.ElementsParser.parse(elements, callback, options, reviver); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) return oStyle; + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param points {String} points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ], i, len; + + // points could look like "10,20 30,40" or "10 20 30 40" + if (asPairs) { + i = 0; + len = points.length; + for (; i < len; i++) { + var pair = points[i].split(','); + parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); + } + } + else { + i = 0; + len = points.length; + for (; i < len; i+=2) { + parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, + rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); + rule = match[1]; + var declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberof fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + loadSVGFromURL: function(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i,'')); + } + if (!xml.documentElement) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberof fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + loadSVGFromString: function(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(//i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + }, + + /** + * Creates markup containing SVG font faces + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '' + ].join(''); + } + + return markup; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @param {fabric.Canvas} canvas instance of fabric.Canvas + * @return {String} + */ + createSVGRefElementsMarkup: function(canvas) { + var markup = [ ]; + + _createSVGPattern(markup, canvas, 'backgroundColor'); + _createSVGPattern(markup, canvas, 'overlayColor'); + + return markup.join(''); + } }); })(typeof exports !== 'undefined' ? exports : this); +fabric.ElementsParser = { + + parse: function(elements, callback, options, reviver) { + + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + + this.instances = new Array(elements.length); + this.numElements = elements.length; + + this.createObjects(); + }, + + createObjects: function() { + for (var i = 0, len = this.elements.length; i < len; i++) { + this.createObject(this.elements[i], i); + } + }, + + createObject: function(el, index) { + var klass = fabric[fabric.util.string.capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + this._createObject(klass, el, index); + } + catch(err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }, + + _createObject: function(klass, el, index) { + if (klass.async) { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + else { + var obj = klass.fromElement(el, this.options); + this.reviver && this.reviver(el, obj); + this.instances.splice(index, 0, obj); + this.checkIfDone(); + } + }, + + createCallback: function(index, el) { + var _this = this; + return function(obj) { + _this.reviver && _this.reviver(el, obj); + _this.instances.splice(index, 0, obj); + _this.checkIfDone(); + }; + }, + + checkIfDone: function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + return el != null; + }); + fabric.resolveGradients(this.instances); + this.callback(this.instances); + } + } +}; + + (function(global) { "use strict"; @@ -7908,12 +8029,33 @@ fabric.util.string = { opacity: isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity) }; } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } /* _FROM_SVG_END_ */ /** * Gradient class * @class fabric.Gradient * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition */ fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { @@ -8130,22 +8272,10 @@ fabric.util.string = { coords = { }; if (type === 'linear') { - coords = { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; + coords = getLinearCoords(el); } else if (type === 'radial') { - coords = { - x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', - y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', - r1: 0, - x2: el.getAttribute('cx') || '50%', - y2: el.getAttribute('cy') || '50%', - r2: el.getAttribute('r') || '50%' - }; + coords = getRadialCoords(el); } for (var i = colorStopEls.length; i--; ) { @@ -8191,13 +8321,17 @@ fabric.util.string = { options[prop] = fabric.util.toFixed(object.height * percents / 100, 2); } } - // normalize rendering point (should be from top/left corner rather than center of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] -= fabric.util.toFixed(object.width / 2, 2); - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] -= fabric.util.toFixed(object.height / 2, 2); - } + normalize(options, prop, object); + } + } + + // normalize rendering point (should be from top/left corner rather than center of the shape) + function normalize(options, prop, object) { + if (prop === 'x1' || prop === 'x2') { + options[prop] -= fabric.util.toFixed(object.width / 2, 2); + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= fabric.util.toFixed(object.height / 2, 2); } } @@ -8207,13 +8341,9 @@ fabric.util.string = { */ function _convertValuesToPercentUnits(object, options) { for (var prop in options) { - // normalize rendering point (should be from center rather than top/left corner of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] += fabric.util.toFixed(object.width / 2, 2); - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] += fabric.util.toFixed(object.height / 2, 2); - } + + normalize(options, prop, object); + // convert to percent units if (prop === 'x1' || prop === 'x2' || prop === 'r2') { options[prop] = fabric.util.toFixed(options[prop] / object.width * 100, 2) + '%'; @@ -8233,6 +8363,7 @@ fabric.util.string = { * @class fabric.Pattern * @see {@link http://fabricjs.com/patterns/|Pattern demo} * @see {@link http://fabricjs.com/dynamic-patterns/|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition */ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { @@ -8363,6 +8494,11 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ toLive: function(ctx) { var source = typeof this.source === 'function' ? this.source() : this.source; + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) return ''; + if (source.naturalWidth === 0 || source.naturalHeight === 0) return ''; + } return ctx.createPattern(source, this.repeat); } }); @@ -8383,6 +8519,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * Shadow class * @class fabric.Shadow * @see {@link http://fabricjs.com/shadows/|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition */ fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { @@ -8420,6 +8557,13 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ affectStroke: false, + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + /** * Constructor * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)") @@ -8497,12 +8641,28 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * @return {Object} Object representation of a shadow instance */ toObject: function() { - return { - color: this.color, - blur: this.blur, - offsetX: this.offsetX, - offsetY: this.offsetY - }; + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + if (this.color !== proto.color) { + obj.color = this.color; + } + if (this.blur !== proto.blur) { + obj.blur = this.blur; + } + if (this.offsetX !== proto.offsetX) { + obj.offsetX = this.offsetX; + } + if (this.offsetY !== proto.offsetY) { + obj.offsetY = this.offsetY; + } + return obj; } }); @@ -8530,7 +8690,6 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ var extend = fabric.util.object.extend, getElementOffset = fabric.util.getElementOffset, removeFromArray = fabric.util.removeFromArray, - removeListener = fabric.util.removeListener, CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); @@ -8540,6 +8699,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * @mixes fabric.Collection * @mixes fabric.Observable * @see {@link http://fabricjs.com/static_canvas/|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition * @fires before:render * @fires after:render * @fires canvas:cleared @@ -8562,56 +8722,43 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ }, /** - * Background color of canvas instance - * @type String + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} * @default */ backgroundColor: '', /** - * Background image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setBackgroundImage} - * @type String + * Background image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. + * Backwards incompatibility note: The "backgroundImageOpacity" + * and "backgroundImageStretch" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. + * @type fabric.Image * @default */ - backgroundImage: '', + backgroundImage: null, /** - * Opacity of the background image of the canvas instance - * @type Float + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} * @default */ - backgroundImageOpacity: 1, + overlayColor: '', /** - * Indicates whether the background image should be stretched to fit the - * dimensions of the canvas instance. - * @type Boolean + * Overlay image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. + * Backwards incompatibility note: The "overlayImageLeft" + * and "overlayImageTop" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#left} and {@link fabric.Image#top}. + * @type fabric.Image * @default */ - backgroundImageStretch: true, - - /** - * Overlay image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setOverlayImage} - * @type String - * @default - */ - overlayImage: '', - - /** - * Left offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageLeft: 0, - - /** - * Top offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageTop: 0, + overlayImage: null, /** * Indicates whether toObject/toDatalessObject should include default values @@ -8693,6 +8840,9 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ if (options.backgroundColor) { this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, this.renderAll.bind(this)); + } this.calcOffset(); }, @@ -8709,74 +8859,115 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {String} url url of an image to set overlay to + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the overlay image - * @param {Number} [options.overlayImageLeft] {@link fabric.StaticCanvas#overlayImageLeft|Left offset} of overlay image - * @param {Number} [options.overlayImageTop] {@link fabric.StaticCanvas#overlayImageTop|Top offset} of overlay image + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Displaced overlayImage (left and top != 0) + * @example Normal overlayImage with left/top = 0 * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * overlayImageLeft: 100, - * overlayImageTop: 100 + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setOverlayImage: function (url, callback, options) { // TODO (kangax): test callback - fabric.util.loadImage(url, function(img) { - this.overlayImage = img; - if (options && ('overlayImageLeft' in options)) { - this.overlayImageLeft = options.overlayImageLeft; - } - if (options && ('overlayImageTop' in options)) { - this.overlayImageTop = options.overlayImageTop; - } - callback && callback(); - }, this); - - return this; + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); }, /** * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {String} url URL of an image to set background to - * @param {Function} callback callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the background image - * @param {Float} [options.backgroundImageOpacity] {@link fabric.StaticCanvas#backgroundImageOpacity|Opacity} of the background image of the canvas instance - * @param {Boolean} [options.backgroundImageStretch] Indicates whether the background image should be {@link fabric.StaticCanvas#backgroundImageStretch|strechted} to fit the canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} - * @example Normal backgroundImage - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Stretched backgroundImage with opacity - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * backgroundImageOpacity: 0.5, - * backgroundImageStretch: true + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setBackgroundImage: function (url, callback, options) { - fabric.util.loadImage(url, function(img) { - this.backgroundImage = img; - if (options && ('backgroundImageOpacity' in options)) { - this.backgroundImageOpacity = options.backgroundImageOpacity; - } - if (options && ('backgroundImageStretch' in options)) { - this.backgroundImageStretch = options.backgroundImageStretch; - } - callback && callback(); - }, this); + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, - return this; + /** + * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); }, /** * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {String|fabric.Pattern} backgroundColor Color or pattern to set background color to - * @param {Function} callback callback to invoke when background color is set + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} @@ -8786,20 +8977,63 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * canvas.setBackgroundColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png' * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); */ setBackgroundColor: function(backgroundColor, callback) { - if (backgroundColor.source) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img) { + this[property] = new fabric.Image(img, options); + callback && callback(); + }, this); + } + else { + this[property] = image; + callback && callback(); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String)} color Object with pattern information or color value + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + if (color.source) { var _this = this; - fabric.util.loadImage(backgroundColor.source, function(img) { - _this.backgroundColor = new fabric.Pattern({ + fabric.util.loadImage(color.source, function(img) { + _this[property] = new fabric.Pattern({ source: img, - repeat: backgroundColor.repeat + repeat: color.repeat, + offsetX: color.offsetX, + offsetY: color.offsetY }); callback && callback(); }); } else { - this.backgroundColor = backgroundColor; + this[property] = color; callback && callback(); } @@ -9108,18 +9342,17 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (this.getActiveObject() === obj) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared'); + } + this.fire('object:removed', { target: obj }); obj.fire('removed'); }, - /** - * Returns an array of objects this instance has - * @return {Array} - */ - getObjects: function () { - return this._objects; - }, - /** * Clears specified context of canvas element * @param {CanvasRenderingContext2D} ctx Context to clear @@ -9170,6 +9403,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ renderAll: function (allOnTop) { var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + var activeGroup = this.getActiveGroup(); if (this.contextTop && this.selection && !this._groupSelector) { this.clearContext(this.contextTop); @@ -9185,50 +9419,15 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ fabric.util.clipContext(this, canvasToDrawOn); } - if (this.backgroundColor) { - canvasToDrawOn.fillStyle = this.backgroundColor.toLive - ? this.backgroundColor.toLive(canvasToDrawOn) - : this.backgroundColor; - - canvasToDrawOn.fillRect( - this.backgroundColor.offsetX || 0, - this.backgroundColor.offsetY || 0, - this.width, - this.height); - } - - if (typeof this.backgroundImage === 'object') { - this._drawBackroundImage(canvasToDrawOn); - } - - var activeGroup = this.getActiveGroup(); - for (var i = 0, length = this._objects.length; i < length; ++i) { - if (!activeGroup || - (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { - this._draw(canvasToDrawOn, this._objects[i]); - } - } - - // delegate rendering to group selection (if one exists) - if (activeGroup) { - //Store objects in group preserving order, then replace - var sortedObjects = []; - this.forEachObject(function (object) { - if (activeGroup.contains(object)) { - sortedObjects.push(object); - } - }); - activeGroup._set('objects', sortedObjects); - this._draw(canvasToDrawOn, activeGroup); - } + this._renderBackground(canvasToDrawOn); + this._renderObjects(canvasToDrawOn, activeGroup); + this._renderActiveGroup(canvasToDrawOn, activeGroup); if (this.clipTo) { canvasToDrawOn.restore(); } - if (this.overlayImage) { - canvasToDrawOn.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop); - } + this._renderOverlay(canvasToDrawOn); if (this.controlsAboveOverlay && this.interactive) { this.drawControls(canvasToDrawOn); @@ -9241,19 +9440,80 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * @private - * @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup */ - _drawBackroundImage: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + _renderObjects: function(ctx, activeGroup) { + for (var i = 0, length = this._objects.length; i < length; ++i) { + if (!activeGroup || + (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { + this._draw(ctx, this._objects[i]); + } + } + }, - if (this.backgroundImageStretch) { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderActiveGroup: function(ctx, activeGroup) { + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + + //Store objects in group preserving order, then replace + var sortedObjects = []; + this.forEachObject(function (object) { + if (activeGroup.contains(object)) { + sortedObjects.push(object); + } + }); + activeGroup._set('objects', sortedObjects); + this._draw(ctx, activeGroup); } - else { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (this.backgroundColor) { + ctx.fillStyle = this.backgroundColor.toLive + ? this.backgroundColor.toLive(ctx) + : this.backgroundColor; + + ctx.fillRect( + this.backgroundColor.offsetX || 0, + this.backgroundColor.offsetY || 0, + this.width, + this.height); + } + if (this.backgroundImage) { + this.backgroundImage.render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + if (this.overlayColor) { + ctx.fillStyle = this.overlayColor.toLive + ? this.overlayColor.toLive(ctx) + : this.overlayColor; + + ctx.fillRect( + this.overlayColor.offsetX || 0, + this.overlayColor.offsetY || 0, + this.width, + this.height); + } + if (this.overlayImage) { + this.overlayImage.render(ctx); } - canvasToDrawOn.restore(); }, /** @@ -9302,11 +9562,11 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Centers object horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - object.set('left', this.getCenter().left); + this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); this.renderAll(); return this; }, @@ -9314,12 +9574,12 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Centers object vertically. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center vertically * @return {fabric.Canvas} thisArg * @chainable */ centerObjectV: function (object) { - object.set('top', this.getCenter().top); + this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); this.renderAll(); return this; }, @@ -9327,12 +9587,28 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Centers object vertically and horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ - centerObject: function (object) { - return this.centerObjectH(object).centerObjectV(object); + centerObject: function(object) { + var center = this.getCenter(); + + this._centerObject(object, new fabric.Point(center.left, center.top)); + this.renderAll(); + return this; + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + return this; }, /** @@ -9371,42 +9647,75 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ if (activeGroup) { this.discardActiveGroup(); } + var data = { - objects: this.getObjects().map(function (instance) { - // TODO (kangax): figure out how to clean this up - var originalValue; - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } - var object = instance[methodName](propertiesToInclude); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, this), - background: (this.backgroundColor && this.backgroundColor.toObject) - ? this.backgroundColor.toObject() - : this.backgroundColor + objects: this._toObjects(methodName, propertiesToInclude) }; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.src; - data.backgroundImageOpacity = this.backgroundImageOpacity; - data.backgroundImageStretch = this.backgroundImageStretch; - } - if (this.overlayImage) { - data.overlayImage = this.overlayImage.src; - data.overlayImageLeft = this.overlayImageLeft; - data.overlayImageTop = this.overlayImageTop; - } + + extend(data, this.__serializeBgOverlay()); + fabric.util.populateWithProperties(this, data, propertiesToInclude); + if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); - activeGroup.forEachObject(function(o) { o.set('active', true) }); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); } return data; }, + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this.getObjects().map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function() { + var data = { + background: (this.backgroundColor && this.backgroundColor.toObject) + ? this.backgroundColor.toObject() + : this.backgroundColor + }; + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject() + : this.overlayColor; + } + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.toObject(); + } + if (this.overlayImage) { + data.overlayImage = this.overlayImage.toObject(); + } + + return data; + }, + /* _TO_SVG_START_ */ /** * Returns SVG representation of canvas @@ -9445,8 +9754,29 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ toSVG: function(options, reviver) { options || (options = { }); + var markup = []; + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + + this._setSVGBgOverlayColor(markup, 'backgroundColor'); + this._setSVGBgOverlayImage(markup, 'backgroundImage'); + + this._setSVGObjects(markup, reviver); + + this._setSVGBgOverlayColor(markup, 'overlayColor'); + this._setSVGBgOverlayImage(markup, 'overlayImage'); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { if (!options.suppressPreamble) { markup.push( '', @@ -9454,53 +9784,42 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' ); } + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { markup.push( - '', - 'Created with Fabric.js ', fabric.version, '', - '', fabric.createSVGFontFacesMarkup(this.getObjects()), fabric.createSVGRefElementsMarkup(this), '' + '', + 'Created with Fabric.js ', fabric.version, '', + '', + fabric.createSVGFontFacesMarkup(this.getObjects()), + fabric.createSVGRefElementsMarkup(this), + '' ); + }, - if (this.backgroundColor && this.backgroundColor.source) { - markup.push( - '' - ); - } - - if (this.backgroundImage) { - markup.push( - '' - ); - } - - if (this.overlayImage) { - markup.push( - '' - ); - } - + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { var activeGroup = this.getActiveGroup(); if (activeGroup) { this.discardActiveGroup(); @@ -9510,30 +9829,52 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ } if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); - activeGroup.forEachObject(function(o) { o.set('active', true) }); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); } - markup.push(''); - - return markup.join(''); }, - /* _TO_SVG_END_ */ /** - * Removes an object from canvas and returns it - * @param {fabric.Object} object Object to remove - * @return {fabric.Object} removed object + * @private */ - remove: function (object) { - // removing active object should fire "selection:cleared" events - if (this.getActiveObject() === object) { - this.fire('before:selection:cleared', { target: object }); - this.discardActiveObject(); - this.fire('selection:cleared'); + _setSVGBgOverlayImage: function(markup, property) { + if (this[property] && this[property].toSVG) { + markup.push(this[property].toSVG()); } - - return fabric.Collection.remove.call(this, object); }, + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + if (this[property] && this[property].source) { + markup.push( + '' + ); + } + else if (this[property] && property === 'overlayColor') { + markup.push( + '' + ); + } + }, + /* _TO_SVG_END_ */ + /** * Moves an object to the bottom of the stack of drawn objects * @param {fabric.Object} object Object to send to back @@ -9570,27 +9911,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ // if object is not on the bottom of stack if (idx !== 0) { - var newIdx; - - if (intersecting) { - newIdx = idx; - - // traverse down the stack looking for the nearest intersecting object - for (var i=idx-1; i>=0; --i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx-1; - } + var newIdx = this._findNewLowerIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); @@ -9599,6 +9920,35 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (var i=idx-1; i>=0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + /** * Moves an object up in stack of drawn objects * @param {fabric.Object} object Object to send @@ -9611,27 +9961,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ // if object is not on top of stack (last item in an array) if (idx !== this._objects.length-1) { - var newIdx; - - if (intersecting) { - newIdx = idx; - - // traverse up the stack looking for the nearest intersecting object - for (var i = idx + 1; i < this._objects.length; ++i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx+1; - } + var newIdx = this._findNewUpperIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); @@ -9640,6 +9970,35 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (var i = idx + 1; i < this._objects.length; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx+1; + } + + return newIdx; + }, + /** * Moves an object to specified level in stack of drawn objects * @param {fabric.Object} object Object to send @@ -9654,27 +10013,13 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ }, /** - * Clears a canvas element and removes all event handlers. + * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { this.clear(); - - if (!this.interactive) return this; - - if (fabric.isTouchSupported) { - removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - if (typeof Event !== 'undefined' && 'remove' in Event) { - Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); - } - } - else { - removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(fabric.window, 'resize', this._onResize); - } + this.interactive && this.removeListeners(); return this; }, @@ -9976,7 +10321,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype var p1 = this._points[0]; var p2 = this._points[1]; - + //if we only have 2 points in the path and they are the same //it means that the user only clicked the canvas without moving the mouse //then we should be drawing a dot. A path isn't drawn between two identical dots @@ -10125,7 +10470,12 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false); var path = this.createPath(pathData); - path.set({ left: originLeft, top: originTop }); + path.set({ + left: originLeft, + top: originTop, + originX: 'center', + originY: 'center' + }); this.canvas.add(path); path.setCoords(); @@ -10217,6 +10567,8 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric radius: this.points[i].radius, left: point.x, top: point.y, + originX: 'center', + originY: 'center', fill: this.points[i].fill }); @@ -10224,7 +10576,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } - var group = new fabric.Group(circles); + var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); group.canvas = this.canvas; this.canvas.add(group); @@ -10359,6 +10711,8 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric height: sprayChunk[j].width, left: sprayChunk[j].x + 1, top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', fill: this.color }); @@ -10371,7 +10725,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects); + var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); group.canvas = this.canvas; this.canvas.add(group); @@ -10525,8 +10879,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab radiansToDegrees = fabric.util.radiansToDegrees, atan2 = Math.atan2, abs = Math.abs, - min = Math.min, - max = Math.max, STROKE_OFFSET = 0.5; @@ -10535,6 +10887,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @class fabric.Canvas * @extends fabric.StaticCanvas * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition * * @fires object:modified * @fires object:rotating @@ -10708,7 +11061,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._groupSelector = null; this._initWrapperElement(); this._createUpperCanvas(); - this._initEvents(); + this._initEventListeners(); this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); @@ -10809,47 +11162,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Boolean} */ isTargetTransparent: function (target, x, y) { - var cacheContext = this.contextCache; - var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; target.hasBorders = target.transparentCorners = false; - this._draw(cacheContext, target); + this._draw(this.contextCache, target); target.hasBorders = hasBorders; target.transparentCorners = transparentCorners; - // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 - if (this.targetFindTolerance > 0) { - if (x > this.targetFindTolerance) { - x -= this.targetFindTolerance; - } - else { - x = 0; - } - if (y > this.targetFindTolerance) { - y -= this.targetFindTolerance; - } - else { - y = 0; - } - } + var isTransparent = fabric.util.isTransparent( + this.contextCache, x, y, this.targetFindTolerance); - var isTransparent = true; - var imageData = cacheContext.getImageData( - x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (var i = 3, l = imageData.data.length; i < l; i += 4) { - var temp = imageData.data[i]; - isTransparent = temp <= 0; - if (isTransparent === false) break; //Stop if colour found - } - - imageData = null; - this.clearContext(cacheContext); + this.clearContext(this.contextCache); return isTransparent; }, @@ -10860,17 +11186,24 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {fabric.Object} target */ _shouldClearSelection: function (e, target) { - var activeGroup = this.getActiveGroup(); + var activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(); return ( - !target || ( - target && - activeGroup && - !activeGroup.contains(target) && - activeGroup !== target && - !e.shiftKey) || ( - target && - !target.evented) + !target + || + (target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e.shiftKey) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) ); }, @@ -10897,20 +11230,35 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private - * @param {Event} e Event object - * @param {fabric.Object} target */ - _setupCurrentTransform: function (e, target) { - if (!target) return; + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; - var action = 'drag', - corner, - pointer = fabric.util.transformPoint( - getPointer(e, this.upperCanvasEl), - fabric.util.invertTransform(this.viewportTransform) - ); + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } - corner = target._findTargetCorner(e, this._offset); + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + + return origin; + }, + + /** + * @private + */ + _getActionFromCorner: function(target, corner) { + var action = 'drag'; if (corner) { action = (corner === 'ml' || corner === 'mr') ? 'scaleX' @@ -10920,23 +11268,24 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ? 'rotate' : 'scale'; } + return action; + }, - var originX = target.originX, - originY = target.originY; + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target) { + if (!target) return; - if (corner === 'ml' || corner === 'tl' || corner === 'bl') { - originX = "right"; - } - else if (corner === 'mr' || corner === 'tr' || corner === 'br') { - originX = "left"; - } - - if (corner === 'tl' || corner === 'mt' || corner === 'tr') { - originY = "bottom"; - } - else if (corner === 'bl' || corner === 'mb' || corner === 'br') { - originY = "top"; - } + var corner = target._findTargetCorner(e, this._offset), + pointer = fabric.util.transformPoint( + getPointer(e, this.upperCanvasEl), + fabric.util.invertTransform(this.viewportTransform) + ), + action = this._getActionFromCorner(target, corner), + origin = this._getOriginFromCorner(target, corner); this._currentTransform = { target: target, @@ -10945,8 +11294,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab scaleY: target.scaleY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, - originX: originX, - originY: originY, + originX: origin.x, + originY: origin.y, ex: pointer.x, ey: pointer.y, left: target.left, @@ -10962,84 +11311,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, - originX: originX, - originY: originY + originX: origin.x, + originY: origin.y }; this._resetCurrentTransform(e); }, - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - * @return {Boolean} - */ - _shouldHandleGroupLogic: function(e, target) { - var activeObject = this.getActiveObject(); - return e.shiftKey && - (this.getActiveGroup() || (activeObject && activeObject !== target)) - && this.selection; - }, - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - */ - _handleGroupLogic: function (e, target) { - if (target === this.getActiveGroup()) { - // if it's a group, find target again, this time skipping group - target = this.findTarget(e, true); - // if even object is not found, bail out - if (!target || target.isType('group')) { - return; - } - } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - if (activeGroup.contains(target)) { - activeGroup.removeWithUpdate(target); - this._resetObjectTransform(activeGroup); - target.set('active', false); - if (activeGroup.size() === 1) { - // remove group alltogether if after removal it only contains 1 object - this.discardActiveGroup(); - } - } - else { - activeGroup.addWithUpdate(target); - this._resetObjectTransform(activeGroup); - } - this.fire('selection:created', { target: activeGroup, e: e }); - activeGroup.set('active', true); - } - else { - // group does not exist - if (this._activeObject) { - // only if there's an active object - if (target !== this._activeObject) { - // and that object is not the actual target - var objects = this.getObjects(); - var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); - var group = new fabric.Group( - isActiveLower ? [ target, this._activeObject ] : [ this._activeObject, target ]); - - this.setActiveGroup(group); - this._activeObject = null; - activeGroup = this.getActiveGroup(); - this.fire('selection:created', { target: activeGroup, e: e }); - } - } - // activate target object in any case - target.set('active', true); - } - - if (activeGroup) { - activeGroup.saveCoords(); - } - }, - /** * Translates object by "setting" its left/top * @private @@ -11068,9 +11346,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _scaleObject: function (x, y, by) { var t = this._currentTransform, offset = this._offset, - target = t.target; - - var lockScalingX = target.get('lockScalingX'), + target = t.target, + lockScalingX = target.get('lockScalingX'), lockScalingY = target.get('lockScalingY'); if (lockScalingX && lockScalingY) return; @@ -11079,6 +11356,94 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); var localMouse = target.toLocalPoint(new fabric.Point(x - offset.left, y - offset.top), t.originX, t.originY); + this._setLocalMouse(localMouse, t); + + // Actually scale the object + this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by); + + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + }, + + /** + * @private + */ + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by) { + var target = transform.target; + + transform.newScaleX = target.scaleX; + transform.newScaleY = target.scaleY; + + if (by === 'equally' && !lockScalingX && !lockScalingY) { + this._scaleObjectEqually(localMouse, target, transform); + } + else if (!by) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + + lockScalingX || target.set('scaleX', transform.newScaleX); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + else if (by === 'x' && !target.get('lockUniScaling')) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + lockScalingX || target.set('scaleX', transform.newScaleX); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + + this._flipObject(transform); + }, + + /** + * @private + */ + _scaleObjectEqually: function(localMouse, target, transform) { + + var dist = localMouse.y + localMouse.x; + + var lastDist = (target.height + (target.strokeWidth)) * transform.original.scaleY + + (target.width + (target.strokeWidth)) * transform.original.scaleX; + + // We use transform.scaleX/Y instead of target.scaleX/Y + // because the object may have a min scale and we'll loose the proportions + transform.newScaleX = transform.original.scaleX * dist / lastDist; + transform.newScaleY = transform.original.scaleY * dist / lastDist; + + target.set('scaleX', transform.newScaleX); + target.set('scaleY', transform.newScaleY); + }, + + /** + * @private + */ + _flipObject: function(transform) { + if (transform.newScaleX < 0) { + if (transform.originX === 'left') { + transform.originX = 'right'; + } + else if (transform.originX === 'right') { + transform.originX = 'left'; + } + } + + if (transform.newScaleY < 0) { + if (transform.originY === 'top') { + transform.originY = 'bottom'; + } + else if (transform.originY === 'bottom') { + transform.originY = 'top'; + } + } + }, + + /** + * @private + */ + _setLocalMouse: function(localMouse, t) { + var target = t.target; + if (t.originX === 'right') { localMouse.x *= -1; } @@ -11103,74 +11468,28 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab // adjust the mouse coordinates when dealing with padding if (abs(localMouse.x) > target.padding) { - if (localMouse.x < 0 ) { + if (localMouse.x < 0) { localMouse.x += target.padding; - } else { + } + else { localMouse.x -= target.padding; } - } else { // mouse is within the padding, set to 0 + } + else { // mouse is within the padding, set to 0 localMouse.x = 0; } if (abs(localMouse.y) > target.padding) { - if (localMouse.y < 0 ) { + if (localMouse.y < 0) { localMouse.y += target.padding; - } else { + } + else { localMouse.y -= target.padding; } - } else { + } + else { localMouse.y = 0; } - - // Actually scale the object - var newScaleX = target.scaleX, newScaleY = target.scaleY; - if (by === 'equally' && !lockScalingX && !lockScalingY) { - var dist = localMouse.y + localMouse.x; - var lastDist = (target.height + (target.strokeWidth)) * t.original.scaleY + - (target.width + (target.strokeWidth)) * t.original.scaleX; - - // We use t.scaleX/Y instead of target.scaleX/Y because the object may have a min scale and we'll loose the proportions - newScaleX = t.original.scaleX * dist/lastDist; - newScaleY = t.original.scaleY * dist/lastDist; - - target.set('scaleX', newScaleX); - target.set('scaleY', newScaleY); - } - else if (!by) { - newScaleX = localMouse.x/(target.width+target.strokeWidth); - newScaleY = localMouse.y/(target.height+target.strokeWidth); - - lockScalingX || target.set('scaleX', newScaleX); - lockScalingY || target.set('scaleY', newScaleY); - } - else if (by === 'x' && !target.get('lockUniScaling')) { - newScaleX = localMouse.x/(target.width + target.strokeWidth); - lockScalingX || target.set('scaleX', newScaleX); - } - else if (by === 'y' && !target.get('lockUniScaling')) { - newScaleY = localMouse.y/(target.height + target.strokeWidth); - lockScalingY || target.set('scaleY', newScaleY); - } - - // Check if we flipped - if (newScaleX < 0) - { - if (t.originX === 'left') - t.originX = 'right'; - else if (t.originX === 'right') - t.originX = 'left'; - } - - if (newScaleY < 0) - { - if (t.originY === 'top') - t.originY = 'bottom'; - else if (t.originY === 'bottom') - t.originY = 'top'; - } - - // Make sure the constraints apply - target.setPositionByOrigin(constraintPosition, t.originX, t.originY); }, /** @@ -11266,48 +11585,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private */ - _findSelectedObjects: function (e) { - var group = [ ], - x1 = this._groupSelector.ex, - y1 = this._groupSelector.ey, - x2 = x1 + this._groupSelector.left, - y2 = y1 + this._groupSelector.top, - currentObject, - selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), - selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), - isClick = x1 === x2 && y1 === y2; - - for (var i = this._objects.length; i--; ) { - currentObject = this._objects[i]; - - if (!currentObject) continue; - - if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || - currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || - currentObject.containsPoint(selectionX1Y1) || - currentObject.containsPoint(selectionX2Y2)) { - - if (this.selection && currentObject.selectable) { - currentObject.set('active', true); - group.push(currentObject); - - // only add one object if it's a click - if (isClick) break; - } - } - } - - // do not create group for 1 element only - if (group.length === 1) { - this.setActiveObject(group[0], e); - } - else if (group.length > 1) { - group = new fabric.Group(group.reverse()); - this.setActiveGroup(group); - group.saveCoords(); - this.fire('selection:created', { target: group }); - this.renderAll(); - } + _isLastRenderedObject: function(e) { + return ( + this.controlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay.visible && + this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && + this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)); }, /** @@ -11318,31 +11602,30 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab findTarget: function (e, skipGroup) { if (this.skipTargetFind) return; - var target, - pointer = this.getPointer(e, true); - - if (this.controlsAboveOverlay && - this.lastRenderedObjectWithControlsAboveOverlay && - this.lastRenderedObjectWithControlsAboveOverlay.visible && - this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && - this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)) { - target = this.lastRenderedObjectWithControlsAboveOverlay; - return target; + if (this._isLastRenderedObject(e)) { + return this.lastRenderedObjectWithControlsAboveOverlay; } // first check current group (if one exists) var activeGroup = this.getActiveGroup(); if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { - target = activeGroup; - return target; + return activeGroup; } - // then check all of the objects on canvas + return this._searchPossibleTargets(e); + }, + + /** + * @private + */ + _searchPossibleTargets: function(e) { + // Cache all targets where their bounding box contains point. - var possibleTargets = []; + var possibleTargets = [], + target, + pointer = this.getPointer(e, true); for (var i = this._objects.length; i--; ) { - if (this._objects[i] && this._objects[i].visible && this._objects[i].evented && @@ -11358,6 +11641,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } } + for (var j = 0, len = possibleTargets.length; j < len; j++) { pointer = this.getPointer(e, true); var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); @@ -11485,6 +11769,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return this.upperCanvasEl; }, + /** + * @private + * @param {Object} object + */ + _setActiveObject: function(object) { + if (this._activeObject) { + this._activeObject.set('active', false); + } + this._activeObject = object; + object.set('active', true); + }, + /** * Sets given object as the only active object on canvas * @param {fabric.Object} object Object to set as an active one @@ -11493,14 +11789,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ setActiveObject: function (object, e) { - if (this._activeObject) { - this._activeObject.set('active', false); - } - this._activeObject = object; - object.set('active', true); - + this._setActiveObject(object); this.renderAll(); - this.fire('object:selected', { target: object, e: e }); object.fire('selected', { e: e }); return this; @@ -11515,32 +11805,53 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Discards currently active object - * @return {fabric.Canvas} thisArg - * @chainable + * @private */ - discardActiveObject: function () { + _discardActiveObject: function() { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = null; + }, + + /** + * Discards currently active object + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + this._discardActiveObject(); + this.renderAll(); + this.fire('selection:cleared', { e: e }); return this; }, + /** + * @private + * @param {fabric.Group} group + */ + _setActiveGroup: function(group) { + this._activeGroup = group; + if (group) { + group.canvas = this; + group._calcBounds(); + group._updateObjectsCoords(); + group.setCoords(); + group.set('active', true); + } + }, + /** * Sets active group to a speicified one * @param {fabric.Group} group Group to set as a current one * @return {fabric.Canvas} thisArg * @chainable */ - setActiveGroup: function (group) { - this._activeGroup = group; + setActiveGroup: function (group, e) { + this._setActiveGroup(group); if (group) { - group.canvas = this; - group._calcBounds(); - group._updateObjectsCoords(); - group.setCoords(); - group.set('active', true); + this.fire('object:selected', { target: group, e: e }); + group.fire('selected', { e: e }); } return this; }, @@ -11554,15 +11865,24 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Removes currently active group - * @return {fabric.Canvas} thisArg + * @private */ - discardActiveGroup: function () { + _discardActiveGroup: function() { var g = this.getActiveGroup(); if (g) { g.destroy(); } - return this.setActiveGroup(null); + this.setActiveGroup(null); + }, + + /** + * Discards currently active group + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function (e) { + this._discardActiveGroup(); + this.fire('selection:cleared', { e: e }); + return this; }, /** @@ -11576,8 +11896,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab for ( ; i < len; i++) { allObjects[i].set('active', false); } - this.discardActiveGroup(); - this.discardActiveObject(); + this._discardActiveGroup(); + this._discardActiveObject(); return this; }, @@ -11585,14 +11905,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * Deactivates all objects and dispatches appropriate events * @return {fabric.Canvas} thisArg */ - deactivateAllWithDispatch: function () { + deactivateAllWithDispatch: function (e) { var activeObject = this.getActiveGroup() || this.getActiveObject(); if (activeObject) { - this.fire('before:selection:cleared', { target: activeObject }); + this.fire('before:selection:cleared', { target: activeObject, e: e }); } this.deactivateAll(); if (activeObject) { - this.fire('selection:cleared'); + this.fire('selection:cleared', { e: e }); } return this; }, @@ -11604,23 +11924,39 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab drawControls: function(ctx) { var activeGroup = this.getActiveGroup(); if (activeGroup) { - ctx.save(); - fabric.Group.prototype.transform.call(activeGroup, ctx); - activeGroup.drawBorders(ctx).drawControls(ctx); - ctx.restore(); + this._drawGroupControls(ctx, activeGroup); } else { - for (var i = 0, len = this._objects.length; i < len; ++i) { - if (!this._objects[i] || !this._objects[i].active) continue; - - ctx.save(); - fabric.Object.prototype.transform.call(this._objects[i], ctx); - this._objects[i].drawBorders(ctx).drawControls(ctx); - ctx.restore(); - - this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; - } + this._drawObjectsControls(ctx); } + }, + + /** + * @private + */ + _drawGroupControls: function(ctx, activeGroup) { + this._drawControls(ctx, activeGroup, 'Group'); + }, + + /** + * @private + */ + _drawObjectsControls: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i] || !this._objects[i].active) continue; + this._drawControls(ctx, this._objects[i], 'Object'); + this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; + } + }, + + /** + * @private + */ + _drawControls: function(ctx, object, klass) { + ctx.save(); + fabric[klass].prototype.transform.call(object, ctx); + object.drawBorders(ctx).drawControls(ctx); + ctx.restore(); } }); @@ -11650,14 +11986,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab (function(){ var cursorMap = [ - 'n-resize', - 'ne-resize', - 'e-resize', - 'se-resize', - 's-resize', - 'sw-resize', - 'w-resize', - 'nw-resize' + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + 'nw-resize' ], cursorOffset = { 'mt': 0, // n @@ -11679,34 +12015,110 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * Adds mouse listeners to canvas * @private */ - _initEvents: function () { - var _this = this; + _initEventListeners: function () { + this._bindEvents(); + + addListener(fabric.window, 'resize', this._onResize); + + // mouse events + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); + + // touch events + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (typeof Event !== 'undefined' && 'add' in Event) { + Event.add(this.upperCanvasEl, 'gesture', this._onGesture); + Event.add(this.upperCanvasEl, 'drag', this._onDrag); + Event.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.add(this.upperCanvasEl, 'shake', this._onShake); + } + }, + + /** + * @private + */ + _bindEvents: function() { this._onMouseDown = this._onMouseDown.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseUp = this._onMouseUp.bind(this); this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + }, - this._onGesture = function(e, s) { - _this.__onTransformGesture(e, s); - }; + /** + * Removes all event listeners + */ + removeListeners: function() { + removeListener(fabric.window, 'resize', this._onResize); - addListener(fabric.window, 'resize', this._onResize); + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); - if (fabric.isTouchSupported) { - addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - if (typeof Event !== 'undefined' && 'add' in Event) { - Event.add(this.upperCanvasEl, 'gesture', this._onGesture); - } - } - else { - addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + if (typeof Event !== 'undefined' && 'remove' in Event) { + Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); + Event.remove(this.upperCanvasEl, 'drag', this._onDrag); + Event.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.remove(this.upperCanvasEl, 'shake', this._onShake); } }, + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, s) { + this.__onTransformGesture && this.__onTransformGesture(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, s) { + this.__onDrag && this.__onDrag(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js wheel event + * @param {Event} [self] Inner Event object + */ + _onMouseWheel: function(e, s) { + this.__onMouseWheel && this.__onMouseWheel(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e,s) { + this.__onOrientationChange && this.__onOrientationChange(e,s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e,s) { + this.__onShake && this.__onShake(e,s); + }, + /** * @private * @param {Event} e Event object fired on mousedown @@ -11714,14 +12126,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _onMouseDown: function (e) { this.__onMouseDown(e); - !fabric.isTouchSupported && addListener(fabric.document, 'mouseup', this._onMouseUp); - fabric.isTouchSupported && addListener(fabric.document, 'touchend', this._onMouseUp); + addListener(fabric.document, 'mouseup', this._onMouseUp); + addListener(fabric.document, 'touchend', this._onMouseUp); - !fabric.isTouchSupported && addListener(fabric.document, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && addListener(fabric.document, 'touchmove', this._onMouseMove); + addListener(fabric.document, 'mousemove', this._onMouseMove); + addListener(fabric.document, 'touchmove', this._onMouseMove); - !fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); }, /** @@ -11731,14 +12143,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _onMouseUp: function (e) { this.__onMouseUp(e); - !fabric.isTouchSupported && removeListener(fabric.document, 'mouseup', this._onMouseUp); - fabric.isTouchSupported && removeListener(fabric.document, 'touchend', this._onMouseUp); + removeListener(fabric.document, 'mouseup', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onMouseUp); - !fabric.isTouchSupported && removeListener(fabric.document, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', this._onMouseMove); + removeListener(fabric.document, 'mousemove', this._onMouseMove); + removeListener(fabric.document, 'touchmove', this._onMouseMove); - !fabric.isTouchSupported && addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); }, /** @@ -11766,16 +12178,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _shouldRender: function(target, pointer) { var activeObject = this.getActiveGroup() || this.getActiveObject(); - return ( + return !!( (target && ( - target.isMoving || - target !== activeObject)) || - (!target && activeObject) || + target.isMoving || + target !== activeObject)) + || + (!target && !!activeObject) + || + (!target && !activeObject && !this._groupSelector) + || (pointer && - this._previousPointer && - this.selection && ( - pointer.x !== this._previousPointer.x || - pointer.y !== this._previousPointer.y)) + this._previousPointer && + this.selection && ( + pointer.x !== this._previousPointer.x || + pointer.y !== this._previousPointer.y)) ); }, @@ -11787,84 +12203,38 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mouseup */ __onMouseUp: function (e) { - var target, - pointer, - render; + var target; if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._isCurrentlyDrawing = false; - if (this.clipTo) { - this.contextTop.restore(); - } - this.freeDrawingBrush.onMouseUp(); - this.fire('mouse:up', { e: e }); + this._onMouseUpInDrawingMode(e); return; } if (this._currentTransform) { - - var transform = this._currentTransform; - - target = transform.target; - if (target._scaling) { - target._scaling = false; - } - - target.setCoords(); - - // only fire :modified event if target coordinates were changed during mousedown-mouseup - if (this.stateful && target.hasStateChanged()) { - this.fire('object:modified', { target: target }); - target.fire('modified'); - } - - if (this._previousOriginX && this._previousOriginY) { - - var originPoint = target.translateToOriginPoint( - target.getCenterPoint(), - this._previousOriginX, - this._previousOriginY); - - target.originX = this._previousOriginX; - target.originY = this._previousOriginY; - - target.left = originPoint.x; - target.top = originPoint.y; - - this._previousOriginX = null; - this._previousOriginY = null; - } + this._finalizeCurrentTransform(); + target = this._currentTransform.target; } else { - pointer = this.getPointer(e, true); + target = this.findTarget(e, true); } - render = this._shouldRender(target, pointer); + var shouldRender = this._shouldRender(target, this.getPointer(e)); - if (this.selection && this._groupSelector) { - // group selection was completed, determine its bounds - this._findSelectedObjects(e); - } - - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords(); - activeGroup.isMoving = false; - this._setCursor(this.defaultCursor); - } - - // clear selection and current transformation - this._groupSelector = null; - this._currentTransform = null; + this._maybeGroupObjects(e); if (target) { target.isMoving = false; } - render && this.renderAll(); + shouldRender && this.renderAll(); + this._handleCursorAndEvent(e, target); + }, + + _handleCursorAndEvent: function(e, target) { this._setCursorFromEvent(e, target); + // TODO: why are we doing this? var _this = this; setTimeout(function () { _this._setCursorFromEvent(e, target); @@ -11874,13 +12244,59 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab target && target.fire('mouseup', { e: e }); }, + /** + * @private + */ + _finalizeCurrentTransform: function() { + + var transform = this._currentTransform; + var target = transform.target; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + + this._restoreOriginXY(target); + }, + + /** + * @private + * @param {Object} target Object to restore + */ + _restoreOriginXY: function(target) { + if (this._previousOriginX && this._previousOriginY) { + + var originPoint = target.translateToOriginPoint( + target.getCenterPoint(), + this._previousOriginX, + this._previousOriginY); + + target.originX = this._previousOriginX; + target.originY = this._previousOriginY; + + target.left = originPoint.x; + target.top = originPoint.y; + + this._previousOriginX = null; + this._previousOriginY = null; + } + }, + /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDownInDrawingMode: function(e) { this._isCurrentlyDrawing = true; - this.discardActiveObject().renderAll(); + this.discardActiveObject(e).renderAll(); if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } @@ -11890,6 +12306,33 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.fire('mouse:down', { e: e }); }, + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var ivt = fabric.util.invertTransform(this.viewportTransform); + pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); + this.freeDrawingBrush.onMouseMove(pointer); + } + this.upperCanvasEl.style.cursor = this.freeDrawingCursor; + this.fire('mouse:move', { e: e }); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + this._isCurrentlyDrawing = false; + if (this.clipTo) { + this.contextTop.restore(); + } + this.freeDrawingBrush.onMouseUp(); + this.fire('mouse:up', { e: e }); + }, + /** * Method that defines the actions when mouse is clic ked on canvas. * The method inits the currentTransform parameters and renders all the @@ -11899,6 +12342,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousedown */ __onMouseDown: function (e) { + // accept only left clicks var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; if (!isLeftClick && !fabric.isTouchSupported) return; @@ -11912,53 +12356,71 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this._currentTransform) return; var target = this.findTarget(e), - pointer = this.getPointer(e, true), - corner, - render; + pointer = this.getPointer(e, true); // save pointer for check in __onMouseUp event this._previousPointer = pointer; - render = this._shouldRender(target, pointer); + var shouldRender = this._shouldRender(target, pointer), + shouldGroup = this._shouldGroup(e, target); if (this._shouldClearSelection(e, target)) { - if (this.selection) { - this._groupSelector = { - ex: pointer.x, - ey: pointer.y, - top: 0, - left: 0 - }; - } - this.deactivateAllWithDispatch(); - target && target.selectable && this.setActiveObject(target, e); + this._clearSelection(e, target, pointer); } - else if (this._shouldHandleGroupLogic(e, target)) { - this._handleGroupLogic(e, target); + else if (shouldGroup) { + this._handleGrouping(e, target); target = this.getActiveGroup(); } - else { - // determine if it's a drag or rotate case - this.stateful && target.saveState(); - - if ((corner = target._findTargetCorner(e, this._offset))) { - this.onBeforeScaleRotate(target); - } - - if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { - this.deactivateAll(); - this.setActiveObject(target, e); - } + if (target && target.selectable && !shouldGroup) { + this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } // we must renderAll so that active image is placed on the top canvas - render && this.renderAll(); + shouldRender && this.renderAll(); this.fire('mouse:down', { target: target, e: e }); target && target.fire('mousedown', { e: e }); }, + /** + * @private + */ + _beforeTransform: function(e, target) { + var corner; + + this.stateful && target.saveState(); + + // determine if it's a drag or rotate case + if ((corner = target._findTargetCorner(e, this._offset))) { + this.onBeforeScaleRotate(target); + } + + if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { + this.deactivateAll(); + this.setActiveObject(target, e); + } + }, + + /** + * @private + */ + _clearSelection: function(e, target, pointer) { + this.deactivateAllWithDispatch(e); + + if (target && target.selectable) { + this.setActiveObject(target, e); + } + else if (this.selection) { + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + } + }, + /** * @private * @param {Object} target Object for that origin is set to center @@ -12009,130 +12471,144 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousemove */ __onMouseMove: function (e) { + var target, pointer; if (this.isDrawingMode) { - if (this._isCurrentlyDrawing) { - var ivt = fabric.util.invertTransform(this.viewportTransform); - pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); - this.freeDrawingBrush.onMouseMove(pointer); - } - this.upperCanvasEl.style.cursor = this.freeDrawingCursor; - this.fire('mouse:move', { e: e }); + this._onMouseMoveInDrawingMode(e); return; } var groupSelector = this._groupSelector; - // We initially clicked in an empty area, so we draw a box for multiple selection. + // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { pointer = this.getPointer(e, true); - groupSelector.left = pointer.x - groupSelector.ex; - groupSelector.top = pointer.y - groupSelector.ey; + groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; + groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + this.renderTop(); } else if (!this._currentTransform) { - // alias style to elimintate unnecessary lookup - var style = this.upperCanvasEl.style; - - // Here we are hovering the canvas then we will determine - // what part of the pictures we are hovering to change the caret symbol. - // We won't do that while dragging or rotating in order to improve the - // performance. target = this.findTarget(e); if (!target || target && !target.selectable) { - // no target - set default cursor - style.cursor = this.defaultCursor; + this.upperCanvasEl.style.cursor = this.defaultCursor; } else { - // set proper cursor this._setCursorFromEvent(e, target); } } else { - // object is being transformed (scaled/rotated/moved/etc.) - pointer = fabric.util.transformPoint( - getPointer(e, this.upperCanvasEl), - fabric.util.invertTransform(this.viewportTransform) - ); - - var x = pointer.x, - y = pointer.y, - reset = false, - centerTransform, - transform = this._currentTransform; - - target = transform.target; - target.isMoving = true; - - if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { - centerTransform = this._shouldCenterTransform(e, target); - - // Switch from a normal resize to center-based - if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || - // Switch from center-based resize to normal one - (!centerTransform && transform.originX === 'center' && transform.originY === 'center') - ) { - this._resetCurrentTransform(e); - reset = true; - } - } - - if (transform.action === 'rotate') { - this._rotateObject(x, y); - - this.fire('object:rotating', { target: target, e: e }); - target.fire('rotating', { e: e }); - } - else if (transform.action === 'scale') { - // rotate object only if shift key is not pressed - // and if it is not a group we are transforming - if ((e.shiftKey || this.uniScaleTransform) && !target.get('lockUniScaling')) { - transform.currentAction = 'scale'; - this._scaleObject(x, y); - } - else { - // Switch from a normal resize to proportional - if (!reset && transform.currentAction === 'scale') { - this._resetCurrentTransform(e, target); - } - - transform.currentAction = 'scaleEqually'; - this._scaleObject(x, y, 'equally'); - } - - this.fire('object:scaling', { target: target, e: e }); - target.fire('scaling', { e: e }); - } - else if (transform.action === 'scaleX') { - this._scaleObject(x, y, 'x'); - - this.fire('object:scaling', { target: target, e: e}); - target.fire('scaling', { e: e }); - } - else if (transform.action === 'scaleY') { - this._scaleObject(x, y, 'y'); - - this.fire('object:scaling', { target: target, e: e}); - target.fire('scaling', { e: e }); - } - else { - this._translateObject(x, y); - - this.fire('object:moving', { target: target, e: e}); - target.fire('moving', { e: e }); - this._setCursor(this.moveCursor); - } - - this.renderAll(); + this._transformObject(e); } + this.fire('mouse:move', { target: target, e: e }); target && target.fire('mousemove', { e: e }); }, + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + + var pointer = fabric.util.transformPoint( + getPointer(e, this.upperCanvasEl), + fabric.util.invertTransform(this.viewportTransform) + ), + transform = this._currentTransform; + + transform.reset = false, + transform.target.isMoving = true; + + this._beforeScaleTransform(e, transform); + this._performTransformAction(e, transform, pointer); + + this.renderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + target = transform.target, + action = transform.action; + + if (action === 'rotate') { + this._rotateObject(x, y); + this._fire('rotating', target, e); + } + else if (action === 'scale') { + this._onScale(e, transform, x, y); + this._fire('scaling', target, e); + } + else if (action === 'scaleX') { + this._scaleObject(x, y, 'x'); + this._fire('scaling', target, e); + } + else if (action === 'scaleY') { + this._scaleObject(x, y, 'y'); + this._fire('scaling', target, e); + } + else { + this._translateObject(x, y); + this._fire('moving', target, e); + this._setCursor(this.moveCursor); + } + }, + + /** + * @private + */ + _fire: function(eventName, target, e) { + this.fire('object:' + eventName, { target: target, e: e}); + target.fire(eventName, { e: e }); + }, + + /** + * @private + */ + _beforeScaleTransform: function(e, transform) { + if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { + var centerTransform = this._shouldCenterTransform(e, transform.target); + + // Switch from a normal resize to center-based + if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || + // Switch from center-based resize to normal one + (!centerTransform && transform.originX === 'center' && transform.originY === 'center') + ) { + this._resetCurrentTransform(e); + transform.reset = true; + } + } + }, + + /** + * @private + */ + _onScale: function(e, transform, x, y) { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + if ((e.shiftKey || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { + transform.currentAction = 'scale'; + this._scaleObject(x, y); + } + else { + // Switch from a normal resize to proportional + if (!transform.reset && transform.currentAction === 'scale') { + this._resetCurrentTransform(e, transform.target); + } + + transform.currentAction = 'scaleEqually'; + this._scaleObject(x, y, 'equally'); + } + }, + /** * Sets the cursor depending on where the canvas is being hovered. * Note: very buggy in Opera @@ -12140,9 +12616,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Object} target Object that the mouse is hovering, if so. */ _setCursorFromEvent: function (e, target) { - var s = this.upperCanvasEl.style; - if (!target) { - s.cursor = this.defaultCursor; + var style = this.upperCanvasEl.style; + + if (!target || !target.selectable) { + style.cursor = this.defaultCursor; return false; } else { @@ -12153,34 +12630,250 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab && target._findTargetCorner(e, this._offset); if (!corner) { - s.cursor = target.hoverCursor || this.hoverCursor; + style.cursor = target.hoverCursor || this.hoverCursor; } else { - if (corner in cursorOffset) { - var n = Math.round((target.getAngle() % 360) / 45); - if (n<0) { - n += 8; // full circle ahead - } - n += cursorOffset[corner]; - // normalize n to be from 0 to 7 - n %= 8; - s.cursor = cursorMap[n]; - } - else if (corner === 'mtr' && target.hasRotatingPoint) { - s.cursor = this.rotationCursor; - } - else { - s.cursor = this.defaultCursor; - return false; - } + this._setCornerCursor(corner, target); } } return true; + }, + + /** + * @private + */ + _setCornerCursor: function(corner, target) { + var style = this.upperCanvasEl.style; + + if (corner in cursorOffset) { + style.cursor = this._getRotatedCornerCursor(corner, target); + } + else if (corner === 'mtr' && target.hasRotatingPoint) { + style.cursor = this.rotationCursor; + } + else { + style.cursor = this.defaultCursor; + return false; + } + }, + + /** + * @private + */ + _getRotatedCornerCursor: function(corner, target) { + var n = Math.round((target.getAngle() % 360) / 45); + + if (n < 0) { + n += 8; // full circle ahead + } + n += cursorOffset[corner]; + // normalize n to be from 0 to 7 + n %= 8; + + return cursorMap[n]; } }); })(); +(function(){ + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this.getActiveObject(); + return e.shiftKey && + (this.getActiveGroup() || (activeObject && activeObject !== target)) + && this.selection; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + + if (target === this.getActiveGroup()) { + + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + if (this.getActiveGroup()) { + this._updateActiveGroup(target, e); + } + else { + this._createActiveGroup(target, e); + } + + if (this._activeGroup) { + this._activeGroup.saveCoords(); + } + }, + + /** + * @private + */ + _updateActiveGroup: function(target, e) { + var activeGroup = this.getActiveGroup(); + + if (activeGroup.contains(target)) { + + activeGroup.removeWithUpdate(target); + this._resetObjectTransform(activeGroup); + target.set('active', false); + + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(e); + // activate last remaining object + this.setActiveObject(activeGroup.item(0)); + return; + } + } + else { + activeGroup.addWithUpdate(target); + this._resetObjectTransform(activeGroup); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.set('active', true); + }, + + /** + * @private + */ + _createActiveGroup: function(target, e) { + + if (this._activeObject && target !== this._activeObject) { + + var group = this._createGroup(target); + + this.setActiveGroup(group); + this._activeObject = null; + + this.fire('selection:created', { target: group, e: e }); + } + + target.set('active', true); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + + var objects = this.getObjects(); + + var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); + + var groupObjects = isActiveLower + ? [ this._activeObject, target ] + : [ target, this._activeObject ]; + + return new fabric.Group(groupObjects, { + originX: 'center', + originY: 'center' + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(); + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + group = new fabric.Group(group.reverse(), { + originX: 'center', + originY: 'center' + }); + this.setActiveGroup(group, e); + group.saveCoords(); + this.fire('selection:created', { target: group }); + this.renderAll(); + } + }, + + /** + * @private + */ + _collectObjects: function() { + var group = [ ], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + isClick = x1 === x2 && y1 === y2; + + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) continue; + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || + currentObject.containsPoint(selectionX1Y1) || + currentObject.containsPoint(selectionX2Y2) + ) { + currentObject.set('active', true); + group.push(currentObject); + + // only add one object if it's a click + if (isClick) break; + } + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords().setCoords(); + activeGroup.isMoving = false; + this._setCursor(this.defaultCursor); + } + + // clear selection and current transformation + this._groupSelector = null; + this._currentTransform = null; + } + }); + +})(); + + fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** @@ -12329,15 +13022,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.deactivateAll(); } + this.renderAll(true); + + var data = this.__toDataURL(format, quality, cropping); + // restoring width, height for `renderAll` to draw // background properly (while context is scaled) this.width = origWidth; this.height = origHeight; - this.renderAll(true); - - var data = this.__toDataURL(format, quality, cropping); - ctx.scale(1 / multiplier, 1 / multiplier); this.setWidth(origWidth).setHeight(origHeight); @@ -12453,72 +13146,74 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var _this = this; this._enlivenObjects(serialized.objects, function () { - _this._setBgOverlayImages(serialized, callback); + _this._setBgOverlay(serialized, callback); }, reviver); return this; }, - _setBgOverlayImages: function(serialized, callback) { - + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { var _this = this, - backgroundPatternLoaded, - backgroundImageLoaded, - overlayImageLoaded; + loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } var cbIfLoaded = function () { - callback && backgroundImageLoaded && overlayImageLoaded && backgroundPatternLoaded && callback(); + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + _this.renderAll(); + callback && callback(); + } }; - if (serialized.backgroundImage) { - this.setBackgroundImage(serialized.backgroundImage, function() { + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); - _this.backgroundImageOpacity = serialized.backgroundImageOpacity; - _this.backgroundImageStretch = serialized.backgroundImageStretch; + cbIfLoaded(); + }, - _this.renderAll(); + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; - backgroundImageLoaded = true; + if (!value) { + loaded[property] = true; + return; + } - cbIfLoaded(); + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.Image.fromObject(value, function(img) { + _this[property] = img; + loaded[property] = true; + callback && callback(); }); } else { - backgroundImageLoaded = true; - } - - if (serialized.overlayImage) { - this.setOverlayImage(serialized.overlayImage, function() { - - _this.overlayImageLeft = serialized.overlayImageLeft || 0; - _this.overlayImageTop = serialized.overlayImageTop || 0; - - _this.renderAll(); - overlayImageLoaded = true; - - cbIfLoaded(); + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); }); } - else { - overlayImageLoaded = true; - } - - if (serialized.background) { - this.setBackgroundColor(serialized.background, function() { - - _this.renderAll(); - backgroundPatternLoaded = true; - - cbIfLoaded(); - }); - } - else { - backgroundPatternLoaded = true; - } - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) { - callback && callback(); - } }, /** @@ -12573,9 +13268,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Clones canvas instance * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children */ - clone: function (callback) { - var data = JSON.stringify(this); + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); this.cloneWithoutData(function(clone) { clone.loadFromJSON(data, function() { callback && callback(clone); @@ -12642,6 +13338,36 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.fire('touch:gesture', {target: target, e: e, self: self}); }, + /** + * Method that defines actions when an Event.js drag is detected. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onDrag: function(e, self) { + this.fire('touch:drag', {e: e, self: self}); + }, + + /** + * Method that defines actions when an Event.js orientation event is detected. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onOrientationChange: function(e, self) { + this.fire('touch:orientation', {e: e, self: self}); + }, + + /** + * Method that defines actions when an Event.js shake event is detected. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onShake: function(e, self) { + this.fire('touch:shake', {e: e, self: self}); + }, + /** * Scales an object by a factor * @param s {Number} The scale factor to apply to the current scale level @@ -12708,6 +13434,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Root object class from which all 2d shape classes inherit from * @class fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#objects} + * @see {@link fabric.Object#initialize} for constructor definition * * @fires added * @fires removed @@ -12998,14 +13725,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type String * @default */ - originX: 'center', + originX: 'left', /** * Vertical origin of transformation of an object (one of "top", "bottom", "center") * @type String * @default */ - originY: 'center', + originY: 'top', /** * Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom} @@ -13137,7 +13864,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type Boolean * @default */ - centeredRotation: false, + centeredRotation: true, /** * Color of object's fill @@ -13490,95 +14217,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this.toObject(propertiesToInclude); }, - /* _TO_SVG_START_ */ - /** - * Returns styles-string for svg-export - * @return {String} - */ - getSvgStyles: function() { - - var fill = this.fill - ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) - : 'none'; - - var stroke = this.stroke - ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) - : 'none'; - - var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; - var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; - var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; - var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; - var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; - var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; - - var visibility = this.visible ? '' : " visibility: hidden;"; - var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; - - return [ - "stroke: ", stroke, "; ", - "stroke-width: ", strokeWidth, "; ", - "stroke-dasharray: ", strokeDashArray, "; ", - "stroke-linecap: ", strokeLineCap, "; ", - "stroke-linejoin: ", strokeLineJoin, "; ", - "stroke-miterlimit: ", strokeMiterLimit, "; ", - "fill: ", fill, "; ", - "opacity: ", opacity, ";", - filter, - visibility - ].join(''); - }, - - /** - * Returns transform-string for svg-export - * @return {String} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - var center = this.getCenterPoint(); - - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var translatePart = "translate(" + - toFixed(center.x, NUM_FRACTION_DIGITS) + - " " + - toFixed(center.y, NUM_FRACTION_DIGITS) + - ")"; - - var anglePart = angle !== 0 - ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") - : ''; - - var scalePart = (this.scaleX === 1 && this.scaleY === 1) - ? '' : - (" scale(" + - toFixed(this.scaleX, NUM_FRACTION_DIGITS) + - " " + - toFixed(this.scaleY, NUM_FRACTION_DIGITS) + - ")"); - - var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; - var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; - - return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); - }, - - _createBaseSVGMarkup: function() { - var markup = [ ]; - - if (this.fill && this.fill.toLive) { - markup.push(this.fill.toSVG(this, false)); - } - if (this.stroke && this.stroke.toLive) { - markup.push(this.stroke.toSVG(this, false)); - } - if (this.shadow) { - markup.push(this.shadow.toSVG(this)); - } - return markup; - }, - /* _TO_SVG_END_ */ - /** * @private * @param {Object} object @@ -13705,6 +14343,27 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ctx.save(); + this._transform(ctx, noTransform); + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + + var m = this.transformMatrix; + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + this._render(ctx, noTransform); + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + ctx.restore(); + + this._renderControls(ctx, noTransform); + }, + + _transform: function(ctx, noTransform) { var m = this.transformMatrix; var v; if (this.canvas) { @@ -13719,11 +14378,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (m && !this.group) { ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); } - if (!noTransform) { this.transform(ctx); } + }, + _setStrokeStyles: function(ctx) { if (this.stroke) { ctx.lineWidth = this.strokeWidth; ctx.lineCap = this.strokeLineCap; @@ -13733,26 +14393,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ? this.stroke.toLive(ctx) : this.stroke; } + }, + _setFillStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive ? this.fill.toLive(ctx) : this.fill; } - - if (m && this.group) { - ctx.translate(-this.group.width/2, -this.group.height/2); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - this._setShadow(ctx); - this.clipTo && fabric.util.clipContext(this, ctx); - this._render(ctx, noTransform); - this.clipTo && ctx.restore(); - this._removeShadow(ctx); - ctx.restore(); - - this._renderControls(ctx, noTransform); }, /** @@ -13933,10 +14581,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.set('active', false); this.setPositionByOrigin(new fabric.Point(el.width / 2, el.height / 2), 'center', 'center'); + var originalCanvas = this.canvas; canvas.add(this); var data = canvas.toDataURL(options); this.set(origParams).setCoords(); + this.canvas = originalCanvas; canvas.dispose(); canvas = null; @@ -14133,7 +14783,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @chainable */ center: function () { - return this.centerH().centerV(); + this.canvas.centerObject(this); + return this; }, /** @@ -14146,81 +14797,18 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Moves an object to the bottom of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) */ - sendToBack: function() { - if (this.group) { - fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); - } - else { - this.canvas.sendToBack(this); - } - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - if (this.group) { - fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); - } - else { - this.canvas.bringToFront(this); - } - return this; - }, - - /** - * Moves an object down in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function(intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); - } - else { - this.canvas.sendBackwards(this, intersecting); - } - return this; - }, - - /** - * Moves an object up in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function(intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); - } - else { - this.canvas.bringForward(this, intersecting); - } - return this; - }, - - /** - * Moves an object to specified level in stack of drawn objects - * @param {Number} index New position of object - * @return {fabric.Object} thisArg - * @chainable - */ - moveTo: function(index) { - if (this.group) { - fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); - } - else { - this.canvas.moveTo(this, index); - } - return this; + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + return { + x: pointer.x - objectLeftTop.x, + y: pointer.y - objectLeftTop.y + }; } }); @@ -14270,20 +14858,22 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { - var cx = point.x, cy = point.y; + var cx = point.x, + cy = point.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; - if ( originX === "left" ) { - cx = point.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) )/ 2; + if (originX === "left") { + cx = point.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - cx = point.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + else if (originX === "right") { + cx = point.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - if ( originY === "top" ) { - cy = point.y +( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2; + if (originY === "top") { + cy = point.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - cy = point.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2; + else if (originY === "bottom") { + cy = point.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the reverse rotation to the point (it's already scaled properly) @@ -14298,20 +14888,22 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { - var x = center.x, y = center.y; + var x = center.x, + y = center.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; // Get the point coordinates - if ( originX === "left" ) { - x = center.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - x = center.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } - if ( originY === "top" ) { - y = center.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2; + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - y = center.y + ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2; + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the rotation to the point (it's already scaled properly) @@ -14349,29 +14941,32 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Returns the point in local coordinates - * @param {fabric.Point} The point relative to the global coordinate system + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ toLocalPoint: function(point, originX, originY) { - var center = this.getCenterPoint(); + var center = this.getCenterPoint(), + strokeWidth = this.stroke ? this.strokeWidth : 0, + x, y; - var x, y; - if (originX !== undefined && originY !== undefined) { - if ( originX === "left" ) { - x = center.x - (this.getWidth() + this.strokeWidth*this.scaleX) / 2; + if (originX && originY) { + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - x = center.x + (this.getWidth() + this.strokeWidth*this.scaleX)/ 2; + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } else { x = center.x; } - if ( originY === "top" ) { - y = center.y - (this.getHeight() + this.strokeWidth*this.scaleY) / 2; + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - y = center.y + (this.getHeight() + this.strokeWidth*this.scaleY)/ 2; + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } else { y = center.y; @@ -14411,7 +15006,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * @param {String} to One of left, center, right + * @param {String} to One of 'left', 'center', 'right' */ adjustPosition: function(to) { var angle = degreesToRadians(this.angle); @@ -14552,7 +15147,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Checks if point is inside the object - * @param {Object} point + * @param {fabric.Point} point Point to check against * @return {Boolean} true if point is inside the object */ containsPoint: function(point) { @@ -14593,8 +15188,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Helper method to determine how many cross points are between the 4 object edges * and the horizontal line determined by a point on canvas * @private - * @param {Object} point - * @param {Object} oCoords Coordinates of the image being evaluated + * @param {fabric.Point} point Point to check + * @param {Object} oCoords Coordinates of the object being evaluated */ _findCrossPoints: function(point, oCoords) { var b1, b2, a1, a2, xi, yi, @@ -14861,6 +15456,185 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati })(); +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group) { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else { + this.canvas.moveTo(this, index); + } + return this; + } +}); + + +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns styles-string for svg-export + * @return {String} + */ + getSvgStyles: function() { + + var fill = this.fill + ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) + : 'none'; + + var stroke = this.stroke + ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) + : 'none'; + + var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; + var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; + var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; + var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; + var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; + var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; + + var visibility = this.visible ? '' : " visibility: hidden;"; + var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + + return [ + "stroke: ", stroke, "; ", + "stroke-width: ", strokeWidth, "; ", + "stroke-dasharray: ", strokeDashArray, "; ", + "stroke-linecap: ", strokeLineCap, "; ", + "stroke-linejoin: ", strokeLineJoin, "; ", + "stroke-miterlimit: ", strokeMiterLimit, "; ", + "fill: ", fill, "; ", + "opacity: ", opacity, ";", + filter, + visibility + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @return {String} + */ + getSvgTransform: function() { + var toFixed = fabric.util.toFixed; + var angle = this.getAngle(); + var center = this.getCenterPoint(); + + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var translatePart = "translate(" + + toFixed(center.x, NUM_FRACTION_DIGITS) + + " " + + toFixed(center.y, NUM_FRACTION_DIGITS) + + ")"; + + var anglePart = angle !== 0 + ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") + : ''; + + var scalePart = (this.scaleX === 1 && this.scaleY === 1) + ? '' : + (" scale(" + + toFixed(this.scaleX, NUM_FRACTION_DIGITS) + + " " + + toFixed(this.scaleY, NUM_FRACTION_DIGITS) + + ")"); + + var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; + var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; + + return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function() { + var markup = [ ]; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + if (this.shadow) { + markup.push(this.shadow.toSVG(this)); + } + return markup; + } +}); +/* _TO_SVG_END_ */ + + /* Depends on `stateProperties` */ @@ -14911,12 +15685,19 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot (function(){ var getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians; + degreesToRadians = fabric.util.degreesToRadians, + isVML = typeof G_vmlCanvasManager !== 'undefined'; fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** - * Determines which one of the four corners has been clicked + * The object interactivity controls. + * @private + */ + _controlsVisibility: null, + + /** + * Determines which corner has been clicked * @private * @param {Event} e Event object * @param {Object} offset Canvas offset @@ -14933,6 +15714,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot for (var i in this.oCoords) { + if (!this.isControlVisible(i)) { + continue; + } + if (i === 'mtr' && !this.hasRotatingPoint) { continue; } @@ -15196,7 +15981,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ~~(h + padding2 + strokeWidth * sy) + 1 ); - if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) { + if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY @@ -15239,9 +16024,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot padding = this.padding, scaleOffset = size2, scaleOffsetSize = size2 - size, - methodName = this.transparentCorners ? 'strokeRect' : 'fillRect', - transparent = this.transparentCorners, - isVML = typeof G_vmlCanvasManager !== 'undefined'; + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; ctx.save(); @@ -15251,78 +16034,139 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left - _left = left - scaleOffset - strokeWidth2 - padding; - _top = top - scaleOffset - strokeWidth2 - padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('tl', ctx, methodName, + left - scaleOffset - strokeWidth2 - padding, + top - scaleOffset - strokeWidth2 - padding); // top-right - _left = left + width - scaleOffset + strokeWidth2 + padding; - _top = top - scaleOffset - strokeWidth2 - padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('tr', ctx, methodName, + left + width - scaleOffset + strokeWidth2 + padding, + top - scaleOffset - strokeWidth2 - padding); // bottom-left - _left = left - scaleOffset - strokeWidth2 - padding; - _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('tr', ctx, methodName, + left - scaleOffset - strokeWidth2 - padding, + top + height + scaleOffsetSize + strokeWidth2 + padding); // bottom-right - _left = left + width + scaleOffsetSize + strokeWidth2 + padding; - _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('br', ctx, methodName, + left + width + scaleOffsetSize + strokeWidth2 + padding, + top + height + scaleOffsetSize + strokeWidth2 + padding); if (!this.get('lockUniScaling')) { - // middle-top - _left = left + width/2 - scaleOffset; - _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + // middle-top + this._drawControl('mt', ctx, methodName, + left + width/2 - scaleOffset, + top - scaleOffset - strokeWidth2 - padding); // middle-bottom - _left = left + width/2 - scaleOffset; - _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('mb', ctx, methodName, + left + width/2 - scaleOffset, + top + height + scaleOffsetSize + strokeWidth2 + padding); // middle-right - _left = left + width + scaleOffsetSize + strokeWidth2 + padding; - _top = top + height/2 - scaleOffset; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('mb', ctx, methodName, + left + width + scaleOffsetSize + strokeWidth2 + padding, + top + height/2 - scaleOffset); // middle-left - _left = left - scaleOffset - strokeWidth2 - padding; - _top = top + height/2 - scaleOffset; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('ml', ctx, methodName, + left - scaleOffset - strokeWidth2 - padding, + top + height/2 - scaleOffset); } // middle-top-rotate if (this.hasRotatingPoint) { - - _left = left + width/2 - scaleOffset; - _top = this.flipY ? - (top + height + (this.rotatingPointOffset) - size2 + strokeWidth2 + padding) - : (top - (this.rotatingPointOffset) - size2 - strokeWidth2 - padding); - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('mtr', ctx, methodName, + left + width/2 - scaleOffset, + this.flipY + ? (top + height + this.rotatingPointOffset - this.cornerSize/2 + strokeWidth2 + padding) + : (top - this.rotatingPointOffset - this.cornerSize/2 - strokeWidth2 - padding)); } ctx.restore(); return this; + }, + + /** + * @private + */ + _drawControl: function(control, ctx, methodName, left, top) { + var size = this.cornerSize; + + if (this.isControlVisible(control)) { + isVML || this.transparentCorners || ctx.clearRect(left, top, size, size); + ctx[methodName](left, top, size, size); + } + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlName) { + return this._getControlsVisibility()[controlName]; + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlName, visible) { + this._getControlsVisibility()[controlName] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + /** + * Returns the instance of the control visibility set for this object. + * @private + * @returns {Object} + */ + _getControlsVisibility: function() { + if (!this._controlsVisibility) { + this._controlsVisibility = { + tl: true, + tr: true, + br: true, + bl: true, + ml: true, + mt: true, + mr: true, + mb: true, + mtr: true + }; + } + return this._controlsVisibility; } }); })(); @@ -15570,6 +16414,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Line class * @class fabric.Line * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition */ fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { @@ -15637,7 +16482,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _render: function(ctx) { ctx.beginPath(); - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); } @@ -15793,6 +16638,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Circle class * @class fabric.Circle * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition */ fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { @@ -15989,6 +16835,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Triangle * @extends fabric.Object * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition */ fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { @@ -16116,6 +16963,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Ellipse * @extends fabric.Object * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition */ fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { @@ -16304,6 +17152,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Rect * @extends fabric.Object * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition */ fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { @@ -16392,7 +17241,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot y = -this.height / 2, w = this.width, h = this.height, - isInPathGroup = this.group && this.group.type !== 'group'; + isInPathGroup = this.group && this.group.type === 'path-group'; ctx.beginPath(); ctx.globalAlpha = isInPathGroup ? (ctx.globalAlpha * this.opacity) : this.opacity; @@ -16569,8 +17418,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot "use strict"; var fabric = global.fabric || (global.fabric = { }), - toFixed = fabric.util.toFixed, - min = fabric.util.array.min; + toFixed = fabric.util.toFixed; if (fabric.Polyline) { fabric.warn('fabric.Polyline is already defined'); @@ -16581,6 +17429,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Polyline class * @class fabric.Polyline * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition */ fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { @@ -16593,10 +17442,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Constructor - * @param {Array} points Array of points + * @param {Array} points Array of points (where each point is an object with x and y) * @param {Object} [options] Options object * @param {Boolean} [skipOffset] Whether points offsetting should be skipped * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); */ initialize: function(points, options, skipOffset) { options = options || { }; @@ -16713,18 +17575,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES), - minX = min(points, 'x'), - minY = min(points, 'y'); + parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES); - minX = minX < 0 ? minX : 0; - minY = minX < 0 ? minY : 0; - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2 + minX) || 0; - points[i].y -= (options.height / 2 + minY) || 0; - } + fabric.util.normalizePoints(points, options); return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options), true); }; @@ -16764,6 +17617,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Polygon class * @class fabric.Polygon * @extends fabric.Object + * @see {@link fabric.Polygon#initialize} for constructor definition */ fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ { @@ -16923,18 +17777,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES), - minX = min(points, 'x'), - minY = min(points, 'y'); + parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); - minX = minX < 0 ? minX : 0; - minY = minX < 0 ? minY : 0; - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2 + minX) || 0; - points[i].y -= (options.height / 2 + minY) || 0; - } + fabric.util.normalizePoints(points, options); return new fabric.Polygon(points, extend(parsedAttributes, options), true); }; @@ -17007,6 +17852,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Path * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition */ fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { @@ -17110,7 +17956,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot tempControlX, tempControlY, l = -((this.width / 2) + this.pathOffset.x), - t = -((this.height / 2) + this.pathOffset.y); + t = -((this.height / 2) + this.pathOffset.y), + methodName; for (var i = 0, len = this.path.length; i < len; ++i) { @@ -17154,14 +18001,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot x += current[1]; y += current[2]; // draw a line if previous command was moveTo as well (otherwise, it will have no effect) - ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t); + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; // draw a line if previous command was moveTo as well (otherwise, it will have no effect) - ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t); + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); break; case 'c': // bezierCurveTo, relative @@ -17215,7 +18068,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot tempY + t ); // set control point to 2nd one of this command - // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." controlX = x + current[1]; controlY = y + current[2]; @@ -17241,7 +18096,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot y = tempY; // set control point to 2nd one of this command - // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." controlX = current[1]; controlY = current[2]; @@ -17403,24 +18260,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (!noTransform) { this.transform(ctx); } - // ctx.globalCompositeOperation = this.fillRule; - - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx) - : this.fill; - } - - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - } - + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); ctx.beginPath(); @@ -17568,49 +18409,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _parseDimensions: function() { var aX = [], aY = [], - previousX, - previousY, - isLowerCase = false, - x, - y; + previous = { }; this.path.forEach(function(item, i) { - if (item[0] !== 'H') { - previousX = (i === 0) ? getX(item) : getX(this.path[i-1]); - } - if (item[0] !== 'V') { - previousY = (i === 0) ? getY(item) : getY(this.path[i-1]); - } - - // lowercased letter denotes relative position; - // transform to absolute - if (item[0] === item[0].toLowerCase()) { - isLowerCase = true; - } - - // last 2 items in an array of coordinates are the actualy x/y (except H/V); - // collect them - - // TODO (kangax): support relative h/v commands - - x = isLowerCase - ? previousX + getX(item) - : item[0] === 'V' - ? previousX - : getX(item); - - y = isLowerCase - ? previousY + getY(item) - : item[0] === 'H' - ? previousY - : getY(item); - - var val = parseInt(x, 10); - if (!isNaN(val)) aX.push(val); - - val = parseInt(y, 10); - if (!isNaN(val)) aY.push(val); - + this._getCoordsFromCommand(item, i, aX, aY, previous); }, this); var minX = min(aX), @@ -17628,6 +18430,51 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }; return o; + }, + + _getCoordsFromCommand: function(item, i, aX, aY, previous) { + var isLowerCase = false; + + if (item[0] !== 'H') { + previous.x = (i === 0) ? getX(item) : getX(this.path[i - 1]); + } + if (item[0] !== 'V') { + previous.y = (i === 0) ? getY(item) : getY(this.path[i - 1]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + var xy = this._getXY(item, isLowerCase, previous); + + var val = parseInt(xy.x, 10); + if (!isNaN(val)) aX.push(val); + + val = parseInt(xy.y, 10); + if (!isNaN(val)) aY.push(val); + }, + + _getXY: function(item, isLowerCase, previous) { + + // last 2 items in an array of coordinates are the actualy x/y (except H/V), collect them + // TODO (kangax): support relative h/v commands + + var x = isLowerCase + ? previous.x + getX(item) + : item[0] === 'V' + ? previous.x + : getX(item); + + var y = isLowerCase + ? previous.y + getY(item) + : item[0] === 'H' + ? previous.y + : getY(item); + + return { x: x, y: y }; } }); @@ -17711,6 +18558,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.PathGroup * @extends fabric.Path * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} + * @see {@link fabric.PathGroup#initialize} for constructor definition */ fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @lends fabric.PathGroup.prototype */ { @@ -17971,6 +18819,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @extends fabric.Object * @mixes fabric.Collection * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#groups} + * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { @@ -18016,26 +18865,28 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @private */ _updateObjectsCoords: function() { - var groupDeltaX = this.left, - groupDeltaY = this.top; + this.forEachObject(this._updateObjectCoords, this); + }, - this.forEachObject(function(object) { + /** + * @private + */ + _updateObjectCoords: function(object) { + var objectLeft = object.getLeft(), + objectTop = object.getTop(); - var objectLeft = object.get('left'), - objectTop = object.get('top'); + object.set({ + originalLeft: objectLeft, + originalTop: objectTop, + left: objectLeft - this.left, + top: objectTop - this.top + }); - object.set('originalLeft', objectLeft); - object.set('originalTop', objectTop); + object.setCoords(); - object.set('left', objectLeft - groupDeltaX); - object.set('top', objectTop - groupDeltaY); - - object.setCoords(); - - // do not display corners of objects enclosed in a group - object.__origHasControls = object.hasControls; - object.hasControls = false; - }, this); + // do not display corners of objects enclosed in a group + object.__origHasControls = object.hasControls; + object.hasControls = false; }, /** @@ -18046,14 +18897,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return '#'; }, - /** - * Returns an array of all objects in this group - * @return {Array} group objects - */ - getObjects: function() { - return this._objects; - }, - /** * Adds an object to a group; Then recalculates group's dimension, position. * @param {Object} object @@ -18065,12 +18908,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._objects.push(object); object.group = this; // since _restoreObjectsState set objects inactive - this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this); + this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); return this; }, + /** + * @private + */ + _setObjectActive: function(object) { + object.set('active', true); + object.group = this; + }, + /** * Removes an object from a group; Then recalculates group's dimension, position. * @param {Object} object @@ -18080,12 +18931,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot removeWithUpdate: function(object) { this._moveFlippedObject(object); this._restoreObjectsState(); + // since _restoreObjectsState set objects inactive - this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this); + this.forEachObject(this._setObjectActive, this); this.remove(object); this._calcBounds(); this._updateObjectsCoords(); + return this; }, @@ -18157,38 +19010,47 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // do not render if object is not visible if (!this.visible) return; - var v = this.canvas.viewportTransform; ctx.save(); - var sxy = fabric.util.transformPoint( - new fabric.Point(this.scaleX, this.scaleY), - v, - true - ), - groupScaleFactor = Math.max(sxy.x, sxy.y); - this.clipTo && fabric.util.clipContext(this, ctx); - //The array is now sorted in order of highest first, so start from end. + // the array is now sorted in order of highest first, so start from end for (var i = 0, len = this._objects.length; i < len; i++) { - - var object = this._objects[i], - originalScaleFactor = object.borderScaleFactor, - originalHasRotatingPoint = object.hasRotatingPoint; - - // do not render if object is not visible - if (!object.visible) continue; - - object.hasRotatingPoint = false; - object.render(ctx); - - object.hasRotatingPoint = originalHasRotatingPoint; + this._renderObject(this._objects[i], ctx); } + this.clipTo && ctx.restore(); this.callSuper('_renderControls', ctx, noTransform); ctx.restore(); }, + /** + * @private + */ + _renderObject: function(object, ctx) { + var v = this.canvas.viewportTransform, + sxy = fabric.util.transformPoint( + new fabric.Point(this.scaleX, this.scaleY), + v, + true + ); + + var originalScaleFactor = object.borderScaleFactor, + originalHasRotatingPoint = object.hasRotatingPoint, + groupScaleFactor = Math.max(sxy.x, sxy.y); + + // do not render if object is not visible + if (!object.visible) return; + + object.borderScaleFactor = groupScaleFactor; + object.hasRotatingPoint = false; + + object.render(ctx); + + object.borderScaleFactor = originalScaleFactor; + object.hasRotatingPoint = originalHasRotatingPoint; + }, + /** * Retores original state of each of group objects (original state is that which was before group was created). * @private @@ -18207,9 +19069,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {fabric.Group} thisArg */ _moveFlippedObject: function(object) { - var oldOriginX = object.get('originX'); - var oldOriginY = object.get('originY'); - var center = object.getCenterPoint(); + var oldOriginX = object.get('originX'), + oldOriginY = object.get('originY'), + center = object.getCenterPoint(); + object.set({ originX: 'center', originY: 'center', @@ -18217,6 +19080,24 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot top: center.y }); + this._toggleFlipping(object); + + var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); + + object.set({ + originX: oldOriginX, + originY: oldOriginY, + left: newOrigin.x, + top: newOrigin.y + }); + + return this; + }, + + /** + * @private + */ + _toggleFlipping: function(object) { if (this.flipX) { object.toggle('flipX'); object.set('left', -object.get('left')); @@ -18227,15 +19108,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot object.set('top', -object.get('top')); object.setAngle(-object.getAngle()); } - - var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); - object.set({ - originX: oldOriginX, - originY: oldOriginY, - left: newOrigin.x, - top: newOrigin.y - }); - return this; }, /** @@ -18245,19 +19117,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {fabric.Group} thisArg */ _restoreObjectState: function(object) { - var groupLeft = this.get('left'), - groupTop = this.get('top'), - groupAngle = this.getAngle() * (Math.PI / 180), - rotatedTop = Math.cos(groupAngle) * object.get('top') * this.get('scaleY') + Math.sin(groupAngle) * object.get('left') * this.get('scaleX'), - rotatedLeft = -Math.sin(groupAngle) * object.get('top') * this.get('scaleY') + Math.cos(groupAngle) * object.get('left') * this.get('scaleX'); - - object.setAngle(object.getAngle() + this.getAngle()); - - object.set('left', groupLeft + rotatedLeft); - object.set('top', groupTop + rotatedTop); - - object.set('scaleX', object.get('scaleX') * this.get('scaleX')); - object.set('scaleY', object.get('scaleY') * this.get('scaleY')); + this._setObjectPosition(object); object.setCoords(); object.hasControls = object.__origHasControls; @@ -18269,6 +19129,37 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; }, + /** + * @private + */ + _setObjectPosition: function(object) { + var groupLeft = this.getLeft(), + groupTop = this.getTop(), + rotated = this._getRotatedLeftTop(object); + + object.set({ + angle: object.getAngle() + this.getAngle(), + left: groupLeft + rotated.left, + top: groupTop + rotated.top, + scaleX: object.get('scaleX') * this.get('scaleX'), + scaleY: object.get('scaleY') * this.get('scaleY') + }); + }, + + /** + * @private + */ + _getRotatedLeftTop: function(object) { + var groupAngle = this.getAngle() * (Math.PI / 180); + return { + left: (-Math.sin(groupAngle) * object.getTop() * this.get('scaleY') + + Math.cos(groupAngle) * object.getLeft() * this.get('scaleX')), + + top: (Math.cos(groupAngle) * object.getTop() * this.get('scaleY') + + Math.sin(groupAngle) * object.getLeft() * this.get('scaleX')) + }; + }, + /** * Destroys a group (restoring state of its objects) * @return {fabric.Group} thisArg @@ -18334,11 +19225,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _calcBounds: function() { var aX = [], aY = [], - minX, minY, maxX, maxY, o, width, height, minXY, maxXY, - i = 0, - len = this._objects.length; + o; - for (; i < len; ++i) { + for (var i = 0, len = this._objects.length; i < len; ++i) { o = this._objects[i]; o.setCoords(); for (var prop in o.oCoords) { @@ -18346,7 +19235,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot aY.push(o.oCoords[prop].y); } } - + + this.set(this._getBounds(aX, aY)); + }, + + /** + * @private + */ + _getBounds: function(aX, aY) { var ivt; if (this.canvas) { ivt = fabric.util.invertTransform(this.canvas.viewportTransform); @@ -18355,18 +19251,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ivt = [1, 0, 0, 1, 0, 0]; console.log('no canvas'); } + var minXY = fabric.util.transformPoint(new fabric.Point(min(aX), min(aY)), ivt), + maxXY = fabric.util.transformPoint(new fabric.Point(max(aX), max(aY)), ivt); - minXY = new fabric.Point(min(aX), min(aY)); - maxXY = new fabric.Point(max(aX), max(aY)); - - minXY = fabric.util.transformPoint(minXY, ivt); - maxXY = fabric.util.transformPoint(maxXY, ivt); - - this.width = (maxXY.x - minXY.x) || 0; - this.height = (maxXY.y - minXY.y) || 0; - - this.left = (minXY.x + maxXY.x) / 2 || 0; - this.top = (minXY.y + maxXY.y) / 2 || 0; + return { + width: (maxXY.x - minXY.x) || 0, + height: (maxXY.y - minXY.y) || 0, + left: (minXY.x + maxXY.x) / 2 || 0, + top: (minXY.y + maxXY.y) / 2 || 0, + }; }, /* _TO_SVG_START_ */ @@ -18467,6 +19360,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Image * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#images} + * @see {@link fabric.Image#initialize} for constructor definition */ fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { @@ -18477,6 +19371,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ type: 'image', + /** + * crossOrigin value (one of "", "anonymous", "allow-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + /** * Constructor * @param {HTMLImageElement | String} element Image element @@ -18489,7 +19391,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this.filters = [ ]; this.callSuper('initialize', options); - this._initElement(element); + + this._initElement(element, options); this._initConfig(options); if (options.filters) { @@ -18527,6 +19430,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; }, + /** + * Sets crossOrigin value (on an instance and corresponding image element) + * @return {fabric.Image} thisArg + * @chainable + */ + setCrossOrigin: function(value) { + this.crossOrigin = value; + this._element.crossOrigin = value; + + return this; + }, + /** * Returns original size of an image * @return {Object} Object with "width" and "height" properties @@ -18557,7 +19472,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot else { v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution } - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); @@ -18593,14 +19508,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _stroke: function(ctx) { ctx.save(); - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - + this._setStrokeStyles(ctx); ctx.beginPath(); ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); ctx.closePath(); @@ -18618,13 +19526,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot h = this.height; ctx.save(); - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; + this._setStrokeStyles(ctx); ctx.beginPath(); fabric.util.drawDashedLine(ctx, x, y, x+w, y, this.strokeDashArray); @@ -18645,7 +19547,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot src: this._originalElement.src || this._originalElement._src, filters: this.filters.map(function(filterObj) { return filterObj && filterObj.toObject(); - }) + }), + crossOrigin: this.crossOrigin }); }, @@ -18668,7 +19571,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot '" transform="translate(' + (-this.width/2) + ' ' + (-this.height/2) + ')', '" width="', this.width, '" height="', this.height, - '">' + '" preserveAspectRatio="none"', + '>' ); if (this.stroke || this.strokeDashArray) { @@ -18811,6 +19715,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); this.setOptions(options); this._setWidthHeight(options); + this._element.crossOrigin = this.crossOrigin; }, /** @@ -18873,28 +19778,13 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Function} [callback] Callback to invoke when an image instance is created */ fabric.Image.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.src; - - /** @ignore */ - img.onload = function() { + fabric.util.loadImage(object.src, function(img) { fabric.Image.prototype._initFilters.call(object, object, function(filters) { object.filters = filters || [ ]; - var instance = new fabric.Image(img, object); callback && callback(instance); - img = img.onload = img.onerror = null; }); - }; - - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback(null, true); - img = img.onload = img.onerror = null; - }; - - img.src = src; + }, null, object.crossOrigin); }; /** @@ -18907,7 +19797,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot fabric.Image.fromURL = function(url, callback, imgOptions) { fabric.util.loadImage(url, function(img) { callback(new fabric.Image(img, imgOptions)); - }); + }, null, imgOptions && imgOptions.crossOrigin); }; /* _FROM_SVG_START_ */ @@ -19097,6 +19987,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Brightness * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Brightness = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { @@ -19173,6 +20071,41 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Convolute * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Convolute = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { @@ -19214,15 +20147,16 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Object} canvasEl Canvas element to apply filter to */ applyTo: function(canvasEl) { - var weights = this.matrix; - var context = canvasEl.getContext('2d'); - var pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height); - var side = Math.round(Math.sqrt(weights.length)); - var halfSide = Math.floor(side/2); - var src = pixels.data; - var sw = pixels.width; - var sh = pixels.height; + var weights = this.matrix, + context = canvasEl.getContext('2d'), + pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side/2), + src = pixels.data, + sw = pixels.width, + sh = pixels.height; // pad output by the convolution matrix var w = sw; @@ -19233,6 +20167,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag // go through the destination image pixels var alphaFac = this.opaque ? 1 : 0; + for (var y=0; y= 0 && scy < sh && scx >= 0 && scx < sw) { - var srcOff = (scy*sw+scx)*4; - var wt = weights[cy*side+cx]; - r += src[srcOff] * wt; - g += src[srcOff+1] * wt; - b += src[srcOff+2] * wt; - a += src[srcOff+3] * wt; - } + + /* jshint maxdepth:5 */ + if (scy < 0 || scy > sh || scx < 0 || scx > sw) continue; + + var srcOff = (scy*sw+scx)*4; + var wt = weights[cy*side+cx]; + + r += src[srcOff] * wt; + g += src[srcOff+1] * wt; + b += src[srcOff+2] * wt; + a += src[srcOff+3] * wt; } } dst[dstOff] = r; @@ -19302,6 +20242,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.GradientTransparency * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.GradientTransparency#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.GradientTransparency({ + * threshold: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.GradientTransparency = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { @@ -19314,7 +20262,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag /** * Constructor - * @memberOf fabric.Image.filters.GradientTransparency + * @memberOf fabric.Image.filters.GradientTransparency.prototype * @param {Object} [options] Options object * @param {Number} [options.threshold=100] Threshold value */ @@ -19376,6 +20324,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Grayscale * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Grayscale = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { @@ -19434,6 +20387,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Invert * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Invert = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { @@ -19490,6 +20448,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Mask * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Mask#initialize} for constructor definition */ fabric.Image.filters.Mask = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { @@ -19564,25 +20523,10 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Function} [callback] Callback to invoke when a mask filter instance is created */ fabric.Image.filters.Mask.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.mask.src; - - /** @ignore */ - img.onload = function() { + fabric.util.loadImage(object.mask.src, function(img) { object.mask = new fabric.Image(img, object.mask); - callback && callback(new fabric.Image.filters.Mask(object)); - img = img.onload = img.onerror = null; - }; - - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback(null, true); - img = img.onload = img.onerror = null; - }; - - img.src = src; + }); }; /** @@ -19608,6 +20552,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Noise * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Noise = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { @@ -19687,6 +20639,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Pixelate * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Pixelate = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { @@ -19791,6 +20751,15 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.RemoveWhite * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveWhite#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveWhite({ + * threshold: 40, + * distance: 140 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.RemoveWhite = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ { @@ -19883,6 +20852,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Sepia * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { @@ -19938,6 +20912,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Sepia2 * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia2(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia2 = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { @@ -19998,6 +20977,21 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Tint * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Tint#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example Tint filter with hex color and opacity + * var filter = new fabric.Image.filters.Tint({ + * color: '#3513B0', + * opacity: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Tint filter with rgba color + * var filter = new fabric.Image.filters.Tint({ + * color: 'rgba(53, 21, 176, 0.5)' + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Tint = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { @@ -20120,6 +21114,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @extends fabric.Object * @return {fabric.Text} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#text} + * @see {@link fabric.Text#initialize} for constructor definition */ fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { @@ -20140,6 +21135,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag text: true }, + /** + * @private + */ + _reNewline: /\r?\n/, + /** * Retrieves object's fontSize * @method getFontSize @@ -20362,7 +21362,8 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag useNative: true, /** - * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) * as well as for history (undo/redo) purposes * @type Array */ @@ -20426,7 +21427,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ _render: function(ctx) { - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); } @@ -20447,31 +21448,20 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderViaNative: function(ctx) { + var textLines = this.text.split(this._reNewline); this.transform(ctx, fabric.isLikelyNode); this._setTextStyles(ctx); - var textLines = this.text.split(/\r?\n/); - this.width = this._getTextWidth(ctx, textLines); this.height = this._getTextHeight(ctx, textLines); this.clipTo && fabric.util.clipContext(this, ctx); this._renderTextBackground(ctx, textLines); - - if (this.textAlign !== 'left' && this.textAlign !== 'justify') { - ctx.save(); - ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); - } - - ctx.save(); - this._setShadow(ctx); - this._renderTextFill(ctx, textLines); - this._renderTextStroke(ctx, textLines); - this._removeShadow(ctx); - ctx.restore(); + this._translateForTextAlign(ctx); + this._renderText(ctx, textLines); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.restore(); @@ -20484,6 +21474,30 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag this._totalLineHeight = 0; }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx, textLines) { + ctx.save(); + this._setShadow(ctx); + this._renderTextFill(ctx, textLines); + this._renderTextStroke(ctx, textLines); + this._removeShadow(ctx); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _translateForTextAlign: function(ctx) { + if (this.textAlign !== 'left' && this.textAlign !== 'justify') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -20510,22 +21524,12 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {CanvasRenderingContext2D} ctx Context to render on */ _setTextStyles: function(ctx) { - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx) - : this.fill; - } - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - } + this._setFillStyles(ctx); + this._setStrokeStyles(ctx); ctx.textBaseline = 'alphabetic'; - ctx.textAlign = this.textAlign; + if (!this.skipTextAlign) { + ctx.textAlign = this.textAlign; + } ctx.font = this._getFontDeclaration(); }, @@ -20546,7 +21550,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @return {Number} Maximum width of fabric.Text object */ _getTextWidth: function(ctx, textLines) { - var maxWidth = ctx.measureText(textLines[0]).width; + var maxWidth = ctx.measureText(textLines[0] || '|').width; for (var i = 1, len = textLines.length; i < len; i++) { var currentLineWidth = ctx.measureText(textLines[i]).width; @@ -20565,7 +21569,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Number} left Left position of text * @param {Number} top Top position of text */ - _drawChars: function(method, ctx, chars, left, top) { + _renderChars: function(method, ctx, chars, left, top) { ctx[method](chars, left, top); }, @@ -20578,13 +21582,13 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Number} top Top position of text * @param {Number} lineIndex Index of a line in a text */ - _drawTextLine: function(method, ctx, line, left, top, lineIndex) { + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // lift the line by quarter of fontSize top -= this.fontSize / 4; // short-circuit if (this.textAlign !== 'justify') { - this._drawChars(method, ctx, line, left, top, lineIndex); + this._renderChars(method, ctx, line, left, top, lineIndex); return; } @@ -20601,12 +21605,12 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag var leftOffset = 0; for (var i = 0, len = words.length; i < len; i++) { - this._drawChars(method, ctx, words[i], left + leftOffset, top, lineIndex); + this._renderChars(method, ctx, words[i], left + leftOffset, top, lineIndex); leftOffset += ctx.measureText(words[i]).width + spaceWidth; } } else { - this._drawChars(method, ctx, line, left, top, lineIndex); + this._renderChars(method, ctx, line, left, top, lineIndex); } }, @@ -20644,7 +21648,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; - this._drawTextLine( + this._renderTextLine( 'fillText', ctx, textLines[i], @@ -20679,7 +21683,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; - this._drawTextLine( + this._renderTextLine( 'strokeText', ctx, textLines[i], @@ -20890,22 +21894,43 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ toSVG: function(reviver) { var markup = [ ], - textLines = this.text.split(/\r?\n/), - lineTopOffset = this.useNative + textLines = this.text.split(this._reNewline), + offsets = this._getSVGLeftTopOffsets(textLines), + textAndBg = this._getSVGTextAndBg(offsets.lineTop, offsets.textLeft, textLines), + shadowSpans = this._getSVGShadows(offsets.lineTop, textLines); + + // move top offset by an ascent + offsets.textTop += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + this._wrapSVGTextAndBg(markup, textAndBg, shadowSpans, offsets); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function(textLines) { + var lineTop = this.useNative ? this.fontSize * this.lineHeight : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), - textLeftOffset = -(this.width/2), - textTopOffset = this.useNative + textLeft = -(this.width/2), + textTop = this.useNative ? this.fontSize - 1 - : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight; - textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), - shadowSpans = this._getSVGShadows(lineTopOffset, textLines); - - // move top offset by an ascent - textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + return { + textLeft: textLeft, + textTop: textTop, + lineTop: lineTop + }; + }, + /** + * @private + */ + _wrapSVGTextAndBg: function(markup, textAndBg, shadowSpans, offsets) { markup.push( '', textAndBg.textBgRects.join(''), @@ -20917,23 +21942,21 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag (this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ': ''), 'style="', this.getSvgStyles(), '" ', /* svg starts from left/bottom corner so we normalize height */ - 'transform="translate(', toFixed(textLeftOffset, 2), ' ', toFixed(textTopOffset, 2), ')">', + 'transform="translate(', toFixed(offsets.textLeft, 2), ' ', toFixed(offsets.textTop, 2), ')">', shadowSpans.join(''), textAndBg.textSpans.join(''), '', '' ); - - return reviver ? reviver(markup.join('')) : markup.join(''); }, /** * @private - * @param {Number} lineTopOffset Line top offset + * @param {Number} lineHeight * @param {Array} textLines Array of all text lines * @return {Array} */ - _getSVGShadows: function(lineTopOffset, textLines) { + _getSVGShadows: function(lineHeight, textLines) { var shadowSpans = [], i, len, lineTopOffsetMultiplier = 1; @@ -20950,14 +21973,15 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag toFixed((lineLeftOffset + lineTopOffsetMultiplier) + this.shadow.offsetX, 2), ((i === 0 || this.useNative) ? '" y' : '" dy'), '="', toFixed(this.useNative - ? ((lineTopOffset * i) - this.height / 2 + this.shadow.offsetY) - : (lineTopOffset + (i === 0 ? this.shadow.offsetY : 0)), 2), + ? ((lineHeight * i) - this.height / 2 + this.shadow.offsetY) + : (lineHeight + (i === 0 ? this.shadow.offsetY : 0)), 2), '" ', this._getFillAttributes(this.shadow.color), '>', fabric.util.string.escapeXml(textLines[i]), ''); lineTopOffsetMultiplier = 1; - } else { + } + else { // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier // prevents empty tspans lineTopOffsetMultiplier++; @@ -20969,15 +21993,79 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag /** * @private - * @param {Number} lineTopOffset Line top offset + * @param {Number} lineHeight * @param {Number} textLeftOffset Text left offset * @param {Array} textLines Array of all text lines * @return {Object} */ - _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { - var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; + _getSVGTextAndBg: function(lineHeight, textLeftOffset, textLines) { + var textSpans = [ ], + textBgRects = [ ], + lineTopOffsetMultiplier = 1; // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + this._setSVGTextLineText(textLines[i], i, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + lineTopOffsetMultiplier = 1; + } + else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + + if (!this.textBackgroundColor || !this._boundaries) continue; + + this._setSVGTextLineBg(textBgRects, i, textLeftOffset, lineHeight); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + _setSVGTextLineText: function(textLine, i, textSpans, lineHeight, lineTopOffsetMultiplier) { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) + ? toFixed(this._boundaries[i].left, 2) + : 0; + + textSpans.push( + ' elements since setting opacity + // on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLine), + '' + ); + }, + + _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) { + textBgRects.push( + ''); + }, + + _setSVGBg: function(textBgRects) { if (this.backgroundColor && this._boundaries) { textBgRects.push( ''); } - - // text and text-background - for (i = 0, len = textLines.length; i < len; i++) { - if (textLines[i] !== '') { - lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; - textSpans.push( - ' elements since setting opacity on containing one doesn't work in Illustrator - this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(textLines[i]), - '' - ); - lineTopOffsetMultiplier = 1; - } - else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - - if (!this.textBackgroundColor || !this._boundaries) continue; - - textBgRects.push( - ''); - } - return { - textSpans: textSpans, - textBgRects: textBgRects - }; }, /** @@ -21220,6 +22265,2629 @@ fabric.util.object.extend(fabric.Text.prototype, { }); +(function() { + + var clone = fabric.util.object.clone; + + /** + * IText class (introduced in v1.4) + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires text:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

Supported key combinations:

+ *
+    *   Move cursor:                    left, right, up, down
+    *   Select character:               shift + left, shift + right
+    *   Select text vertically:         shift + up, shift + down
+    *   Move cursor by word:            alt + left, alt + right
+    *   Select words:                   shift + alt + left, shift + alt + right
+    *   Move cursor to line start/end:  cmd + left, cmd + right
+    *   Select till start/end of line:  cmd + shift + left, cmd + shift + right
+    *   Jump to start/end of text:      cmd + up, cmd + down
+    *   Select till start/end of text:  cmd + shift + up, cmd + shift + down
+    *   Delete character:               backspace
+    *   Delete word:                    alt + backspace
+    *   Delete line:                    cmd + backspace
+    *   Forward delete:                 delete
+    *   Copy text:                      ctrl/cmd + c
+    *   Paste text:                     ctrl/cmd + v
+    *   Cut text:                       ctrl/cmd + x
+    * 
+ * + *

Supported mouse/touch combination

+ *
+    *   Position cursor:                click/touch
+    *   Create selection:               click/touch & drag
+    *   Create selection:               click & shift + click
+    *   Select word:                    double click
+    *   Select line:                    triple click
+    * 
+ */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', + + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Nubmer + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Nubmer + * @default + */ + selectionEnd: 0, + + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', + + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, + + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, + + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of default cursor (when not overwritten by character style) + * @type String + * @default + */ + cursorColor: '#333', + + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Object containing character styles + * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) + * @type Object + * @default + */ + styles: null, + + skipFillStrokeCheck: true, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _fontSizeFraction: 4, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + _charWidthsCache: { }, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.callSuper('initialize', text, options); + this.initBehavior(); + + fabric.IText.instances.push(this); + + // caching + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * Returns true if object has no styling + */ + isEmptyStyles: function() { + if (!this.styles) return true; + var obj = this.styles; + + for (var p1 in obj) { + for (var p2 in obj[p1]) { + /*jshint unused:false */ + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + this.selectionStart = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + this.selectionEnd = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index); + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * @return {Object} styles Style object at a cursor position + */ + getSelectionStyles: function() { + var loc = this.get2DCursorLocation(); + if (this.styles[loc.lineIndex]) { + return this.styles[loc.lineIndex][loc.charIndex] || { }; + } + return { }; + }, + + /** + * Sets style of a current selection + * @param {Object} [styles] Styles object + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles) { + if (this.selectionStart === this.selectionEnd) { + this._extendStyles(this.selectionStart, styles); + } + else { + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + this._extendStyles(i, styles); + } + } + return this; + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this.styles[loc.lineIndex]) { + this.styles[loc.lineIndex] = { }; + } + if (!this.styles[loc.lineIndex][loc.charIndex]) { + this.styles[loc.lineIndex][loc.charIndex] = { }; + } + + fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + this.ctx = ctx; + this.isEditing && this.renderCursorOrSelection(); + }, + + /** + * Renders cursor or selection (depending on what exists) + */ + renderCursorOrSelection: function() { + if (!this.active) return; + + var chars = this.text.split(''), + boundaries; + + if (this.selectionStart === this.selectionEnd) { + boundaries = this._getCursorBoundaries(chars, 'cursor'); + this.renderCursor(boundaries); + } + else { + boundaries = this._getCursorBoundaries(chars, 'selection'); + this.renderSelection(chars, boundaries); + } + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + */ + get2DCursorLocation: function(selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var textBeforeCursor = this.text.slice(0, selectionStart); + var linesBeforeCursor = textBeforeCursor.split(this._reNewline); + + return { + lineIndex: linesBeforeCursor.length - 1, + charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length + }; + }, + + /** + * Returns fontSize of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {Number} Character font size + */ + getCurrentCharFontSize: function(lineIndex, charIndex) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fontSize) || this.fontSize; + }, + + /** + * Returns color (fill) of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {String} Character color (fill) + */ + getCurrentCharColor: function(lineIndex, charIndex) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor; + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(chars, typeOfBoundaries) { + + var cursorLocation = this.get2DCursorLocation(), + + textLines = this.text.split(this._reNewline), + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + left = Math.round(this._getLeftOffset()), + top = -this.height / 2, + + offsets = this._getCursorBoundariesOffsets( + chars, typeOfBoundaries, cursorLocation, textLines); + + return { + left: left, + top: top, + leftOffset: offsets.left + offsets.lineLeft, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries, cursorLocation, textLines) { + + var lineLeftOffset = 0, + + lineIndex = 0, + charIndex = 0, + + leftOffset = 0, + topOffset = typeOfBoundaries === 'cursor' + // selection starts at the very top of the line, + // whereas cursor starts at the padding created by line height + ? (this._getHeightOfLine(this.ctx, 0) - + this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)) + : 0; + + for (var i = 0; i < this.selectionStart; i++) { + if (chars[i] === '\n') { + leftOffset = 0; + var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0); + topOffset += this._getCachedLineHeight(index); + + lineIndex++; + charIndex = 0; + } + else { + leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); + charIndex++; + } + + lineLeftOffset = this._getCachedLineOffset(lineIndex, textLines); + } + + this._clearCache(); + + return { + top: topOffset, + left: leftOffset, + lineLeft: lineLeftOffset + }; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * @private + */ + _getCachedLineHeight: function(index) { + return this.__lineHeights[index] || + (this.__lineHeights[index] = this._getHeightOfLine(this.ctx, index)); + }, + + /** + * @private + */ + _getCachedLineWidth: function(lineIndex, textLines) { + return this.__lineWidths[lineIndex] || + (this.__lineWidths[lineIndex] = this._getWidthOfLine(this.ctx, lineIndex, textLines)); + }, + + /** + * @private + */ + _getCachedLineOffset: function(lineIndex, textLines) { + var widthOfLine = this._getCachedLineWidth(lineIndex, textLines); + + return this.__lineOffsets[lineIndex] || + (this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine)); + }, + + /** + * Renders cursor + * @param {Object} boundaries + */ + renderCursor: function(boundaries) { + var ctx = this.ctx; + + ctx.save(); + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getCurrentCharFontSize(lineIndex, charIndex); + + ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); + ctx.globalAlpha = this._currentCursorOpacity; + + ctx.fillRect( + boundaries.left + boundaries.leftOffset, + boundaries.top + boundaries.topOffset, + this.cursorWidth / this.scaleX, + charHeight); + + ctx.restore(); + }, + + /** + * Renders text selection + * @param {Array} chars Array of characters + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + */ + renderSelection: function(chars, boundaries) { + var ctx = this.ctx; + + ctx.save(); + + ctx.fillStyle = this.selectionColor; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + textLines = this.text.split(this._reNewline), + origLineIndex = lineIndex; + + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + + if (chars[i] === '\n') { + boundaries.leftOffset = 0; + boundaries.topOffset += this._getHeightOfLine(ctx, lineIndex); + lineIndex++; + charIndex = 0; + } + else if (i !== this.text.length) { + + var charWidth = this._getWidthOfChar(ctx, chars[i], lineIndex, charIndex), + lineOffset = this._getLineLeftOffset(this._getWidthOfLine(ctx, lineIndex, textLines)) || 0; + + if (lineIndex === origLineIndex) { + // only offset the line if we're rendering selection of 2nd, 3rd, etc. line + lineOffset = 0; + } + + ctx.fillRect( + boundaries.left + boundaries.leftOffset + lineOffset, + boundaries.top + boundaries.topOffset, + charWidth, + this._getHeightOfLine(ctx, lineIndex)); + + boundaries.leftOffset += charWidth; + charIndex++; + } + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + + if (this.isEmptyStyles()) { + return this._renderCharsFast(method, ctx, line, left, top); + } + + this.skipTextAlign = true; + + // set proper box offset + left -= this.textAlign === 'center' + ? (this.width / 2) + : (this.textAlign === 'right') + ? this.width + : 0; + + // set proper line offset + var textLines = this.text.split(this._reNewline), + lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines), + lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth), + chars = line.split(''); + + left += lineLeftOffset || 0; + + ctx.save(); + for (var i = 0, len = chars.length; i < len; i++) { + this._renderChar(method, ctx, lineIndex, i, chars[i], left, top, lineHeight); + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderCharsFast: function(method, ctx, line, left, top) { + this.skipTextAlign = false; + + if (method === 'fillText' && this.fill) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + if (method === 'strokeText' && this.stroke) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { + var decl, charWidth; + + if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { + + var shouldStroke = decl.stroke || this.stroke, + shouldFill = decl.fill || this.fill; + + ctx.save(); + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl); + + if (shouldFill) { + ctx.fillText(_char, left, top); + } + if (shouldStroke) { + ctx.strokeText(_char, left, top); + } + + this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight); + ctx.restore(); + + ctx.translate(charWidth, 0); + } + else { + if (method === 'strokeText' && this.stroke) { + ctx[method](_char, left, top); + } + if (method === 'fillText' && this.fill) { + ctx[method](_char, left, top); + } + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i); + this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight); + + ctx.translate(ctx.measureText(_char).width, 0); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight) { + var textDecoration = styleDeclaration + ? (styleDeclaration.textDecoration || this.textDecoration) + : this.textDecoration; + + if (!textDecoration) return; + + if (textDecoration.indexOf('underline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + 0 + ); + } + if (textDecoration.indexOf('line-through') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + (lineHeight / this._fontSizeFraction) + ); + } + if (textDecoration.indexOf('overline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top, + charWidth, + lineHeight - (this.fontSize / this._fontSizeFraction) + ); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset) { + ctx.fillRect(left, top - offset, charWidth, 1); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine + top += this.fontSize / 4; + this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines + */ + _renderTextDecoration: function(ctx, textLines) { + if (this.isEmptyStyles()) { + return this.callSuper('_renderTextDecoration', ctx, textLines); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextLinesBackground: function(ctx, textLines) { + if (!this.textBackgroundColor && !this.styles) return; + + ctx.save(); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + } + + var lineHeights = 0, + fractionOfFontSize = this.fontSize / this._fontSizeFraction; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var heightOfLine = this._getHeightOfLine(ctx, i, textLines); + if (textLines[i] === '') { + lineHeights += heightOfLine; + continue; + } + + var lineWidth = this._getWidthOfLine(ctx, i, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset, + this._getTopOffset() + lineHeights + fractionOfFontSize, + lineWidth, + heightOfLine + ); + } + if (this.styles[i]) { + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { + + var _char = textLines[i][j]; + + ctx.fillStyle = this.styles[i][j].textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines), + this._getTopOffset() + lineHeights + fractionOfFontSize, + this._getWidthOfChar(ctx, _char, i, j, textLines) + 1, + heightOfLine + ); + } + } + } + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + */ + _getCacheProp: function(_char, styleDeclaration) { + return _char + + + styleDeclaration.fontFamily + + styleDeclaration.fontSize + + styleDeclaration.fontWeight + + styleDeclaration.fontStyle + + + styleDeclaration.shadow; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} _char + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} [decl] + */ + _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { + var styleDeclaration = decl || (this.styles[lineIndex] && this.styles[lineIndex][charIndex]); + + if (styleDeclaration) { + // cloning so that original style object is not polluted with following font declarations + styleDeclaration = clone(styleDeclaration); + } + else { + styleDeclaration = { }; + } + + this._applyFontStyles(styleDeclaration); + + var cacheProp = this._getCacheProp(_char, styleDeclaration); + + // short-circuit if no styles + if (this.isEmptyStyles() && this._charWidthsCache[cacheProp]) { + return this._charWidthsCache[cacheProp]; + } + + if (typeof styleDeclaration.shadow === 'string') { + styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); + } + + var fill = styleDeclaration.fill || this.fill; + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx) + : fill; + + if (styleDeclaration.stroke) { + ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) + ? styleDeclaration.stroke.toLive(ctx) + : styleDeclaration.stroke; + } + + ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; + ctx.font = this._getFontDeclaration.call(styleDeclaration); + this._setShadow.call(styleDeclaration, ctx); + + if (!this._charWidthsCache[cacheProp]) { + this._charWidthsCache[cacheProp] = ctx.measureText(_char).width; + } + return this._charWidthsCache[cacheProp]; + }, + + /** + * @private + * @param {Object} styleDeclaration + */ + _applyFontStyles: function(styleDeclaration) { + if (!styleDeclaration.fontFamily) { + styleDeclaration.fontFamily = this.fontFamily; + } + if (!styleDeclaration.fontSize) { + styleDeclaration.fontSize = this.fontSize; + } + if (!styleDeclaration.fontWeight) { + styleDeclaration.fontWeight = this.fontWeight; + } + if (!styleDeclaration.fontStyle) { + styleDeclaration.fontStyle = this.fontStyle; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { + ctx.save(); + var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); + ctx.restore(); + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfChar: function(ctx, _char, lineIndex, charIndex) { + if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) { + return this.styles[lineIndex][charIndex].fontSize || this.fontSize; + } + return this.fontSize; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getWidthOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getHeightOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) { + var width = 0; + for (var i = 0; i < charIndex; i++) { + width += this._getWidthOfCharAt(ctx, lineIndex, i, lines); + } + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfLine: function(ctx, lineIndex, textLines) { + // if (!this.styles[lineIndex]) { + // return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]); + // } + return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextWidth: function(ctx, textLines) { + + if (this.isEmptyStyles()) { + return this.callSuper('_getTextWidth', ctx, textLines); + } + + var maxWidth = this._getWidthOfLine(ctx, 0, textLines); + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = this._getWidthOfLine(ctx, i, textLines); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfLine: function(ctx, lineIndex, textLines) { + + textLines = textLines || this.text.split(this._reNewline); + + var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0); + + var line = textLines[lineIndex]; + var chars = line.split(''); + + for (var i = 1, len = chars.length; i < len; i++) { + var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i); + if (currentCharHeight > maxHeight) { + maxHeight = currentCharHeight; + } + } + + return maxHeight * this.lineHeight; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextHeight: function(ctx, textLines) { + var height = 0; + for (var i = 0, len = textLines.length; i < len; i++) { + height += this._getHeightOfLine(ctx, i, textLines); + } + return height; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTopOffset: function() { + var topOffset = fabric.Text.prototype._getTopOffset.call(this); + return topOffset - (this.fontSize / this._fontSizeFraction); + }, + + /** + * Returns object representation of an instance + * @methd toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { + styles: clone(this.styles) + }); + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @return {fabric.IText} instance of fabric.IText + */ + fabric.IText.fromObject = function(object) { + return new fabric.IText(object.text, clone(object)); + }; + + fabric.IText.instances = [ ]; + +})(); + + +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initKeyHandlers(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.initHiddenTextarea(); + }, + + /** + * Initializes "selected" event handler + */ + initSelectedHandler: function() { + this.on('selected', function() { + + var _this = this; + setTimeout(function() { + _this.selected = true; + }, 100); + + if (!this._hasCanvasHandlers) { + this._initCanvasHandlers(); + this._hasCanvasHandlers = true; + } + }); + }, + + /** + * @private + */ + _initCanvasHandlers: function() { + var _this = this; + + this.canvas.on('selection:cleared', function(options) { + + // do not exit editing if event fired + // when clicking on an object again (in editing mode) + if (options.e && _this.canvas.containsPoint(options.e, _this)) return; + + _this.exitEditing(); + }); + + this.canvas.on('mouse:up', function() { + this.getObjects('i-text').forEach(function(obj) { + obj.__isMousedown = false; + }); + }); + }, + + /** + * @private + */ + _tick: function() { + + var _this = this; + + if (this._abortCursorAnimation) return; + + this.animate('_currentCursorOpacity', 1, { + + duration: this.cursorDuration, + + onComplete: function() { + _this._onTickComplete(); + }, + + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, + + /** + * @private + */ + _onTickComplete: function() { + if (this._abortCursorAnimation) return; + + var _this = this; + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this.animate('_currentCursorOpacity', 0, { + duration: this.cursorDuration / 2, + onComplete: function() { + _this._tick(); + }, + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function() { + var _this = this; + if (this._cursorTimeout2) { + clearTimeout(this._cursorTimeout2); + } + this._cursorTimeout2 = setTimeout(function() { + _this._abortCursorAnimation = false; + _this._tick(); + }, this.cursorDelay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + this._abortCursorAnimation = true; + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + this.canvas && this.canvas.renderAll(); + + var _this = this; + setTimeout(function() { + _this._abortCursorAnimation = false; + }, 10); + }, + + /** + * Selects entire text + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this.text.length; + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this.text.slice(this.selectionStart, this.selectionEnd); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Surrent selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index--; + } + } + while (/\S/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index++; + } + } + while (/\S/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} current selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} current selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Returns number of newlines in selected text + * @return {Number} Number of newlines in selected text + */ + getNumNewLinesInSelectedText: function() { + var selectedText = this.getSelectedText(); + var numNewLines = 0; + for (var i = 0, chars = selectedText.split(''), len = chars.length; i < len; i++) { + if (chars[i] === '\n') { + numNewLines++; + } + } + return numNewLines; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction: 1 or -1 + */ + searchWordBoundary: function(selectionStart, direction) { + var index = selectionStart; + var _char = this.text.charAt(index); + var reNonWord = /[ \n\.,;!\?\-]/; + + while (!reNonWord.test(_char) && index > 0 && index < this.text.length) { + index += direction; + _char = this.text.charAt(index); + } + if (reNonWord.test(_char) && _char !== '\n') { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + var newSelectionStart = this.searchWordBoundary(selectionStart, -1); /* search backwards */ + var newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + */ + selectLine: function(selectionStart) { + var newSelectionStart = this.findLineBoundaryLeft(selectionStart); + var newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function() { + if (this.isEditing || !this.editable) return; + + this.exitEditingOnOthers(); + + this.isEditing = true; + + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + + this._tick(); + this.canvas && this.canvas.renderAll(); + + this.fire('editing:entered'); + + return this; + }, + + exitEditingOnOthers: function() { + fabric.IText.instances.forEach(function(obj) { + if (obj === this) return; + obj.exitEditing(); + }, this); + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * @private + */ + _updateTextarea: function() { + if (!this.hiddenTextarea) return; + + this.hiddenTextarea.value = this.text; + this.hiddenTextarea.selectionStart = this.selectionStart; + this.hiddenTextarea.focus(); + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) return; + + this.hoverCursor = this._savedProps.overCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + + this.selected = false; + this.isEditing = false; + this.selectable = true; + + this.selectionEnd = this.selectionStart; + this.hiddenTextarea && this.hiddenTextarea.blur(); + + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + + this.fire('editing:exited'); + + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + var textLines = this.text.split(this._reNewline); + for (var prop in this.styles) { + if (!textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * @private + */ + _removeCharsFromTo: function(start, end) { + + var i = end; + while (i !== start) { + + var prevIndex = this.get2DCursorLocation(i).charIndex; + i--; + var index = this.get2DCursorLocation(i).charIndex; + var isNewline = index > prevIndex; + + if (isNewline) { + this.removeStyleObject(isNewline, i + 1); + } + else { + this.removeStyleObject(this.get2DCursorLocation(i).charIndex === 0, i); + } + + } + + this.text = this.text.slice(0, start) + + this.text.slice(end); + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + * @param {String} _chars Characters to insert + */ + insertChars: function(_chars) { + var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n'; + + this.text = this.text.slice(0, this.selectionStart) + + _chars + + this.text.slice(this.selectionEnd); + + if (this.selectionStart === this.selectionEnd) { + this.insertStyleObject(_chars, isEndOfLine); + } + else if (this.selectionEnd - this.selectionStart > 1) { + // TODO: replace styles properly + // console.log('replacing MORE than 1 char'); + } + + this.selectionStart += _chars.length; + this.selectionEnd = this.selectionStart; + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('text:changed'); + }, + + /** + * Inserts new style object + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { + + this.shiftLineStyles(lineIndex, +1); + + if (!this.styles[lineIndex + 1]) { + this.styles[lineIndex + 1] = { }; + } + + var currentCharStyle = this.styles[lineIndex][charIndex - 1], + newLineStyles = { }; + + // if there's nothing after cursor, + // we clone current char style onto the next (otherwise empty) line + if (isEndOfLine) { + newLineStyles[0] = clone(currentCharStyle); + this.styles[lineIndex + 1] = newLineStyles; + } + // otherwise we clone styles of all chars + // after cursor onto the next line, from the beginning + else { + for (var index in this.styles[lineIndex]) { + if (parseInt(index, 10) >= charIndex) { + newLineStyles[parseInt(index, 10) - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + delete this.styles[lineIndex][index]; + } + } + this.styles[lineIndex + 1] = newLineStyles; + } + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + */ + insertCharStyleObject: function(lineIndex, charIndex) { + + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = clone(currentLineStyles); + + if (charIndex === 0) { + charIndex = 1; + } + + // shift all char styles by 1 forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex]; + //delete currentLineStyles[index]; + } + } + this.styles[lineIndex][charIndex] = clone(currentLineStyles[charIndex - 1]); + }, + + /** + * Inserts style object + * @param {String} _chars Characters at the location where style is inserted + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertStyleObject: function(_chars, isEndOfLine) { + + // short-circuit + if (this.isEmptyStyles()) return; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = { }; + } + + if (_chars === '\n') { + this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); + } + else { + // TODO: support multiple style insertion if _chars.length > 1 + this.insertCharStyleObject(lineIndex, charIndex); + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can be -1 or +1 + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by 1 upward + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + } + } + }, + + /** + * Removes style object + * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line + * @param {Number} [index] Optional index. When not given, current selectionStart is used. + */ + removeStyleObject: function(isBeginningOfLine, index) { + + var cursorLocation = this.get2DCursorLocation(index), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (isBeginningOfLine) { + + var textLines = this.text.split(this._reNewline), + textOnPreviousLine = textLines[lineIndex - 1], + newCharIndexOnPrevLine = textOnPreviousLine.length; + + if (!this.styles[lineIndex - 1]) { + this.styles[lineIndex - 1] = { }; + } + + for (charIndex in this.styles[lineIndex]) { + this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine] + = this.styles[lineIndex][charIndex]; + } + + this.shiftLineStyles(lineIndex, -1); + } + else { + var currentLineStyles = this.styles[lineIndex]; + + if (currentLineStyles) { + var offset = this.selectionStart === this.selectionEnd ? -1 : 0; + delete currentLineStyles[charIndex + offset]; + // console.log('deleting', lineIndex, charIndex + offset); + } + + var currentLineStylesCloned = clone(currentLineStyles); + + // shift all styles by 1 backwards + for (var i in currentLineStylesCloned) { + var numericIndex = parseInt(i, 10); + if (numericIndex >= charIndex && numericIndex !== 0) { + currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex]; + delete currentLineStyles[numericIndex]; + } + } + } + }, + + /** + * Inserts new line + */ + insertNewline: function() { + this.insertChars('\n'); + } + }); +})(); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.lastPointer = { }; + + this.on('mousedown', this.onMouseDown.bind(this)); + }, + + onMouseDown: function(options) { + + this.__newClickTime = +new Date(); + var newPointer = this.canvas.getPointer(options.e); + + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + else if (this.isDoubleClick(newPointer)) { + this.fire('dblclick', options); + this._stopEvent(options.e); + } + + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + }, + + isDoubleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + /** + * @private + */ + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, + + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initSelectedHandler(); + this.initMousedownHandler(); + this.initMousemoveHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('dblclick', function(options) { + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }); + this.on('tripleclick', function(options) { + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }); + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', function(options) { + + var pointer = this.canvas.getPointer(options.e); + + this.__mousedownX = pointer.x; + this.__mousedownY = pointer.y; + this.__isMousedown = true; + + if (this.hiddenTextarea && this.canvas) { + this.canvas.wrapperEl.appendChild(this.hiddenTextarea); + } + + if (this.isEditing) { + this.setCursorByClick(options.e); + this.__selectionStartOnMouseDown = this.selectionStart; + } + else { + this.exitEditingOnOthers(); + } + }); + }, + + /** + * Initializes "mousemove" event handler + */ + initMousemoveHandler: function() { + this.on('mousemove', function(options) { + if (!this.__isMousedown || !this.isEditing) return; + + var newSelectionStart = this.getSelectionStartFromPointer(options.e); + + if (newSelectionStart >= this.__selectionStartOnMouseDown) { + this.setSelectionStart(this.__selectionStartOnMouseDown); + this.setSelectionEnd(newSelectionStart); + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(this.__selectionStartOnMouseDown); + } + }); + }, + + /** + * @private + */ + _isObjectMoved: function(e) { + var pointer = this.canvas.getPointer(e); + + return this.__mousedownX !== pointer.x || + this.__mousedownY !== pointer.y; + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', function(options) { + this.__isMousedown = false; + + if (this._isObjectMoved(options.e)) return; + + if (this.selected) { + this.enterEditing(); + } + }); + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Object} pointer Pointer object with x and y numeric properties + */ + setCursorByClick: function(e) { + var newSelectionStart = this.getSelectionStartFromPointer(e); + + if (e.shiftKey) { + if (newSelectionStart < this.selectionStart) { + this.setSelectionEnd(this.selectionStart); + this.setSelectionStart(newSelectionStart); + } + else { + this.setSelectionEnd(newSelectionStart); + } + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionStart); + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {Object} Object with x/y corresponding to local offset (according to object rotation) + */ + _getLocalRotatedPointer: function(e) { + var pointer = this.canvas.getPointer(e), + + pClicked = new fabric.Point(pointer.x, pointer.y), + pLeftTop = new fabric.Point(this.left, this.top), + + rotated = fabric.util.rotatePoint( + pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle)); + + return this.getLocalPointer(e, rotated); + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + + var mouseOffset = this._getLocalRotatedPointer(e), + textLines = this.text.split(this._reNewline), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + newSelectionStart; + + for (var i = 0, len = textLines.length; i < len; i++) { + + height += this._getHeightOfLine(this.ctx, i) * this.scaleY; + + var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfLine); + + width = lineLeftOffset; + + if (this.flipX) { + // when oject is horizontally flipped we reverse chars + textLines[i] = textLines[i].split('').reverse().join(''); + } + + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + + var _char = textLines[i][j]; + prevWidth = width; + + width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) * + this.scaleX; + + if (height <= mouseOffset.y || width <= mouseOffset.x) { + charIndex++; + continue; + } + + return this._getNewSelectionStartFromOffset( + mouseOffset, prevWidth, width, charIndex + i, jlen); + } + } + + // clicked somewhere after all chars, so set at the end + if (typeof newSelectionStart === 'undefined') { + return this.text.length; + } + }, + + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1, + newSelectionStart = index + offset; + + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this.text.length) { + newSelectionStart = this.text.length; + } + + return newSelectionStart; + } +}); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes key handlers + */ + initKeyHandlers: function() { + fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(fabric.document, 'keypress', this.onKeyPress.bind(this)); + }, + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px'; + + fabric.document.body.appendChild(this.hiddenTextarea); + }, + + /** + * @private + */ + _keysMap: { + 8: 'removeChars', + 13: 'insertNewline', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + 46: 'forwardDelete' + }, + + /** + * @private + */ + _ctrlKeysMap: { + 65: 'selectAll', + 67: 'copy', + 86: 'paste', + 88: 'cut' + }, + + /** + * Handles keyup event + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) return; + + if (e.keyCode in this._keysMap) { + this[this._keysMap[e.keyCode]](e); + } + else if ((e.keyCode in this._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) { + this[this._ctrlKeysMap[e.keyCode]](e); + } + else { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.canvas && this.canvas.renderAll(); + }, + + /** + * Forward delete + */ + forwardDelete: function(e) { + if (this.selectionStart === this.selectionEnd) { + this.moveCursorRight(e); + } + this.removeChars(e); + }, + + /** + * Copies selected text + */ + copy: function() { + var selectedText = this.getSelectedText(); + this.copiedText = selectedText; + }, + + /** + * Pastes text + */ + paste: function() { + if (this.copiedText) { + this.insertChars(this.copiedText); + } + }, + + /** + * Cuts text + */ + cut: function(e) { + this.copy(); + this.removeChars(e); + }, + + /** + * Handles keypress event + * @param {Event} e Event object + */ + onKeyPress: function(e) { + if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) { + return; + } + + this.insertChars(String.fromCharCode(e.which)); + + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * Gets start offset of a selection + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset, + + textBeforeCursor = this.text.slice(0, selectionProp), + textAfterCursor = this.text.slice(selectionProp), + + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1], + textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '', + + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on last line, down cursor goes to end of line + if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey) { + + // move to the end of a text + return this.text.length - selectionProp; + } + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnNextLine = this._getIndexOnNextLine( + cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnSameLineAfterCursor.length + 1 + indexOnNextLine; + }, + + /** + * @private + */ + _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex + 1; + var widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfNextLine); + var widthOfCharsOnNextLine = lineLeftOffset; + var indexOnNextLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) { + + var _char = textOnNextLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnNextLine += widthOfChar; + + if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnNextLine - widthOfChar; + var rightEdge = widthOfCharsOnNextLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j; + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnNextLine = textOnNextLine.length; + } + + return indexOnNextLine; + }, + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorDownWithShift(offset); + } + else { + this.moveCursorDownWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor down without keeping selection + * @param {Number} offset + */ + moveCursorDownWithoutShift: function(offset) { + + this._selectionDirection = 'right'; + this.selectionStart += offset; + + if (this.selectionStart > this.text.length) { + this.selectionStart = this.text.length; + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor down while keeping selection + * @param {Number} offset + */ + moveCursorDownWithShift: function(offset) { + + if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) { + this.selectionStart += offset; + this._selectionDirection = 'left'; + return; + } + else { + this._selectionDirection = 'right'; + this.selectionEnd += offset; + + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + getUpCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on first line, up cursor goes to start of line + if (cursorLocation.lineIndex === 0 || e.metaKey) { + return selectionProp; + } + + var textBeforeCursor = this.text.slice(0, selectionProp), + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '', + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset; + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnPrevLine = this._getIndexOnPrevLine( + cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length; + }, + + /** + * @private + */ + _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex - 1; + var widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine); + var widthOfCharsOnPreviousLine = lineLeftOffset; + var indexOnPrevLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) { + + var _char = textOnPreviousLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnPreviousLine += widthOfChar; + + if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnPreviousLine - widthOfChar; + var rightEdge = widthOfCharsOnPreviousLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnPrevLine = textOnPreviousLine.length - 1; + } + + return indexOnPrevLine; + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorUpWithShift(offset); + } + else { + this.moveCursorUpWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor up with shift + * @param {Number} offset + */ + moveCursorUpWithShift: function(offset) { + + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + else { + if (this._selectionDirection === 'right') { + this.selectionEnd -= offset; + this._selectionDirection = 'right'; + return; + } + else { + this.selectionStart -= offset; + } + } + + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorUpWithoutShift: function(offset) { + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + this.selectionEnd = this.selectionStart; + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorLeftWithShift(e); + } + else { + this.moveCursorLeftWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * @private + */ + _move: function(e, prop, direction) { + if (e.altKey) { + this[prop] = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey) { + this[prop] = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += (direction === 'Left' ? -1 : 1); + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart) { + this._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + this._moveLeft(e, 'selectionEnd'); + } + else { + this._selectionDirection = 'left'; + this._moveLeft(e, 'selectionStart'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionStart) === '\n') { + this.selectionStart--; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorRightWithShift(e); + } + else { + this.moveCursorRightWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + } + else { + this._selectionDirection = 'right'; + this._moveRight(e, 'selectionEnd'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionEnd - 1) === '\n') { + this.selectionEnd++; + } + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e + */ + moveCursorRightWithoutShift: function(e) { + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += this.getNumNewLinesInSelectedText(); + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + this.selectionStart = this.selectionEnd; + } + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + */ + removeChars: function(e) { + if (this.selectionStart === this.selectionEnd) { + this._removeCharsNearCursor(e); + } + else { + this._removeCharsFromTo(this.selectionStart, this.selectionEnd); + } + + this.selectionEnd = this.selectionStart; + + this._removeExtraneousStyles(); + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('text:changed'); + }, + + /** + * @private + */ + _removeCharsNearCursor: function(e) { + if (this.selectionStart !== 0) { + + if (e.metaKey) { + // remove all till the start of current line + var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftLineBoundary, this.selectionStart); + this.selectionStart = leftLineBoundary; + } + else if (e.altKey) { + // remove all till the start of current word + var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftWordBoundary, this.selectionStart); + this.selectionStart = leftWordBoundary; + } + else { + var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n'; + this.removeStyleObject(isBeginningOfLine); + + this.selectionStart--; + this.text = this.text.slice(0, this.selectionStart) + + this.text.slice(this.selectionStart + 1); + } + } + } +}); + + +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * @private + */ + _setSVGTextLineText: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + if (!this.styles[lineIndex]) { + this.callSuper('_setSVGTextLineText', + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier); + } + else { + this._setSVGTextLineChars( + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + } + }, + + /** + * @private + */ + _setSVGTextLineChars: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + + var yProp = lineIndex === 0 || this.useNative ? 'y' : 'dy', + chars = textLine.split(''), + charOffset = 0, + lineLeftOffset = this._getSVGLineLeftOffset(lineIndex), + lineTopOffset = this._getSVGLineTopOffset(lineIndex), + heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); + + for (var i = 0, len = chars.length; i < len; i++) { + var styleDecl = this.styles[lineIndex][i] || { }; + + textSpans.push( + this._createTextCharSpan( + chars[i], styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset)); + + var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); + + if (styleDecl.textBackgroundColor) { + textBgRects.push( + this._createTextCharBg( + styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset)); + } + + charOffset += charWidth; + } + }, + + /** + * @private + */ + _getSVGLineLeftOffset: function(lineIndex) { + return (this._boundaries && this._boundaries[lineIndex]) + ? fabric.util.toFixed(this._boundaries[lineIndex].left, 2) + : 0; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0; + for (var j = 0; j <= lineIndex; j++) { + lineTopOffset += this._getHeightOfLine(this.ctx, j); + } + return lineTopOffset - this.height / 2; + }, + + /** + * @private + */ + _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { + return [ + '' + ].join(''); + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset) { + + var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ + visible: true, + fill: this.fill, + stroke: this.stroke, + type: 'text' + }, styleDecl)); + + return [ + '', + + fabric.util.string.escapeXml(_char), + '' + ].join(''); + } +}); +/* _TO_SVG_END_ */ + + (function() { if (typeof document !== 'undefined' && typeof window !== 'undefined') { @@ -21309,6 +24977,9 @@ fabric.util.object.extend(fabric.Text.prototype, { else if (url) { request(url, 'binary', createImageAndCallBack); } + else { + callback && callback.call(context, url); + } }; fabric.loadSVGFromURL = function(url, callback, reviver) { diff --git a/dist/all.min.js b/dist/all.min.js index 8db3ed73..59f4cf05 100644 --- a/dist/all.min.js +++ b/dist/all.min.js @@ -1,6 +1,7 @@ -/* build: `node build.js modules=ALL exclude=gestures` *//*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */var fabric=fabric||{version:"1.3.7"};typeof exports!="undefined"&&(exports.fabric=fabric),typeof document!="undefined"&&typeof window!="undefined"?(fabric.document=document,fabric.window=window):(fabric.document=require("jsdom").jsdom(""),fabric.window=fabric.document.createWindow()),fabric.isTouchSupported="ontouchstart"in fabric.document.documentElement,fabric.isLikelyNode=typeof Buffer!="undefined"&&typeof window=="undefined";var Cufon=function(){function r(e){var t=this.face=e.face;this.glyphs=e.glyphs,this.w=e.w,this.baseSize=parseInt(t["units-per-em"],10),this.family=t["font-family"].toLowerCase(),this.weight=t["font-weight"],this.style=t["font-style"]||"normal",this.viewBox=function(){var e=t.bbox.split(/\s+/),n={minX:parseInt(e[0],10),minY:parseInt(e[1],10),maxX:parseInt(e[2],10),maxY:parseInt(e[3],10)};return n.width=n.maxX-n.minX,n.height=n.maxY-n.minY,n.toString=function(){return[this.minX,this.minY,this.width,this.height].join(" ")},n}(),this.ascent=-parseInt(t.ascent,10),this.descent=-parseInt(t.descent,10),this.height=-this.ascent+this.descent}function i(){var e={},t={oblique:"italic",italic:"oblique"};this.add=function(t){(e[t.style]||(e[t.style]={}))[t.weight]=t},this.get=function(n,r){var i=e[n]||e[t[n]]||e.normal||e.italic||e.oblique;if(!i)return null;r={normal:400,bold:700}[r]||parseInt(r,10);if(i[r])return i[r];var s={1:1,99:0}[r%100],o=[],u,a;s===undefined&&(s=r>400),r==500&&(r=400);for(var f in i){f=parseInt(f,10);if(!u||fa)a=f;o.push(f)}return ra&&(r=a),o.sort(function(e,t){return(s?e>r&&t>r?et:et:e=i.length+e?r():setTimeout(arguments.callee,10)}),function(t){e?t():n.push(t)}}(),supports:function(e,t){var n=fabric.document.createElement("span").style;return n[e]===undefined?!1:(n[e]=t,n[e]===t)},textAlign:function(e,t,n,r){return t.get("textAlign")=="right"?n>0&&(e=" "+e):nk&&(k=N),A.push(N),N=0;continue}var O=t.glyphs[T[b]]||t.missingGlyph;if(!O)continue;N+=C=Number(O.w||t.w)+h}A.push(N),N=Math.max(k,N);var M=[];for(var b=A.length;b--;)M[b]=N-A[b];if(C===null)return null;d+=l.width-C,m+=l.minX;var _,D;if(f)_=u,D=u.firstChild;else{_=fabric.document.createElement("span"),_.className="cufon cufon-canvas",_.alt=n,D=fabric.document.createElement("canvas"),_.appendChild(D);if(i.printable){var P=fabric.document.createElement("span");P.className="cufon-alt",P.appendChild(fabric.document.createTextNode(n)),_.appendChild(P)}}var H=_.style,B=D.style||{},j=c.convert(l.height-p+v),F=Math.ceil(j),I=F/j;D.width=Math.ceil(c.convert(N+d-m)*I),D.height=F,p+=l.minY,B.top=Math.round(c.convert(p-t.ascent))+"px",B.left=Math.round(c.convert(m))+"px";var q=Math.ceil(c.convert(N*I)),R=q+"px",U=c.convert(t.height),z=(i.lineHeight-1)*c.convert(-t.ascent/5)*(L-1);Cufon.textOptions.width=q,Cufon.textOptions.height=U*L+z,Cufon.textOptions.lines=L,Cufon.textOptions.totalLineHeight=z,e?(H.width=R,H.height=U+"px"):(H.paddingLeft=R,H.paddingBottom=U-1+"px");var W=Cufon.textOptions.context||D.getContext("2d"),X=F/l.height;Cufon.textOptions.fontAscent=t.ascent*X,Cufon.textOptions.boundaries=null;for(var V=Cufon.textOptions.shadowOffsets,b=y.length;b--;)V[b]=[y[b][0]*X,y[b][1]*X];W.save(),W.scale(X,X),W.translate(-m-1/X*D.width/2+(Cufon.fonts[t.family].offsetLeft||0),-p-Cufon.textOptions.height/X/2+(Cufon.fonts[t.family].offsetTop||0)),W.lineWidth=t.face["underline-thickness"],W.save();var J=Cufon.getTextDecoration(i),K=i.fontStyle==="italic";W.save(),Q();if(g)for(var b=0,w=g.length;b.cufon-vml-canvas{text-indent:0}@media screen{cvml\\:shape,cvml\\:shadow{behavior:url(#default#VML);display:block;antialias:true;position:absolute}.cufon-vml-canvas{position:absolute;text-align:left}.cufon-vml{display:inline-block;position:relative;vertical-align:middle}.cufon-vml .cufon-alt{position:absolute;left:-10000in;font-size:1px}a .cufon-vml{cursor:pointer}}@media print{.cufon-vml *{display:none}.cufon-vml .cufon-alt{display:inline}}'),function(e,t,i,s,o,u,a){var f=t===null;f&&(t=o.alt);var l=e.viewBox,c=i.computedFontSize||(i.computedFontSize=new Cufon.CSS.Size(n(u,i.get("fontSize"))+"px",e.baseSize)),h=i.computedLSpacing;h==undefined&&(h=i.get("letterSpacing"),i.computedLSpacing=h=h=="normal"?0:~~c.convertFrom(r(u,h)));var p,d;if(f)p=o,d=o.firstChild;else{p=fabric.document.createElement("span"),p.className="cufon cufon-vml",p.alt=t,d=fabric.document.createElement("span"),d.className="cufon-vml-canvas",p.appendChild(d);if(s.printable){var v=fabric.document.createElement("span");v.className="cufon-alt",v.appendChild(fabric.document.createTextNode(t)),p.appendChild(v)}a||p.appendChild(fabric.document.createElement("cvml:shape"))}var m=p.style,g=d.style,y=c.convert(l.height),b=Math.ceil(y),w=b/y,E=l.minX,S=l.minY;g.height=b,g.top=Math.round(c.convert(S-e.ascent)),g.left=Math.round(c.convert(E)),m.height=c.convert(e.height)+"px";var x=Cufon.getTextDecoration(s),T=i.get("color"),N=Cufon.CSS.textTransform(t,i).split(""),C=0,k=0,L=null,A,O,M=s.textShadow;for(var _=0,D=0,P=N.length;_-1},complexity:function(){return this.getObjects().reduce(function(e,t){return e+=t.complexity?t.complexity():0,e},0)}},function(e){function r(e,t){var n=e.indexOf(t);return n!==-1&&e.splice(n,1),e}function i(e,t){return Math.floor(Math.random()*(t-e+1))+e}function o(e){return e*s}function u(e){return e/s}function a(e,t,n){var r=Math.sin(n),i=Math.cos(n);e.subtractEquals(t);var s=e.x*i-e.y*r,o=e.x*r+e.y*i;return(new fabric.Point(s,o)).addEquals(t)}function f(e,t){return parseFloat(Number(e).toFixed(t))}function l(){return!1}function c(e,t){return e=fabric.util.string.camelize(e.charAt(0).toUpperCase()+e.slice(1)),h(t)[e]}function h(t){if(!t)return fabric;var n=t.split("."),r=n.length,i=e||fabric.window;for(var s=0;s1?r=new fabric.PathGroup(e,t):r=e[0],typeof n!="undefined"&&r.setSourcePath(n),r}function m(e,t,n){if(n&&Object.prototype.toString.call(n)==="[object Array]")for(var r=0,i=n.length;rr)r+=u[p++%h],r>l&&(r=l),e[d?"lineTo":"moveTo"](r,0),d=!d;e.restore()}function y(e){return e||(e=fabric.document.createElement("canvas")),!e.getContext&&typeof G_vmlCanvasManager!="undefined"&&G_vmlCanvasManager.initElement(e),e}function b(){return fabric.isLikelyNode?new(require("canvas").Image):fabric.document.createElement("img")}function w(e){var t=e.prototype;for(var n=t.stateProperties.length;n--;){var r=t.stateProperties[n],i=r.charAt(0).toUpperCase()+r.slice(1),s="set"+i,o="get"+i;t[o]||(t[o]=function(e){return new Function('return this.get("'+e+'")')}(r)),t[s]||(t[s]=function(e){return new Function("value",'return this.set("'+e+'", value)')}(r))}}function E(e,t){t.save(),t.beginPath(),e.clipTo(t),t.clip()}function S(e,t){var n=[[e[0],e[2],e[4]],[e[1],e[3],e[5]],[0,0,1]],r=[[t[0],t[2],t[4]],[t[1],t[3],t[5]],[0,0,1]],i=[];for(var s=0;s<3;s++){i[s]=[];for(var o=0;o<3;o++){var u=0;for(var a=0;a<3;a++)u+=n[s][a]*r[a][o];i[s][o]=u}}return[i[0][0],i[1][0],i[0][1],i[1][1],i[0][2],i[1][2]]}function x(e){return(String(e).match(/function[^{]*\{([\s\S]*)\}/)||{})[1]}function T(e,t,n,r){var i=r[0],s=r[1],o=r[2],u=r[3],a=r[4],f=r[5],l=r[6],c=A(f,l,i,s,u,a,o,t,n);for(var h=0;h1&&(d=Math.sqrt(d),n*=d,r*=d);var v=c/n,m=l/n,g=-l/r,y=c/r,b=v*u+m*a,w=g*u+y*a,E=v*e+m*t,S=g*e+y*t,x=(E-b)*(E-b)+(S-w)*(S-w),T=1/x-.25;T<0&&(T=0);var C=Math.sqrt(T);s===i&&(C=-C);var A=.5*(b+E)-C*(S-w),O=.5*(w+S)+C*(E-b),M=Math.atan2(w-O,b-A),_=Math.atan2(S-O,E-A),D=_-M;D<0&&s===1?D+=2*Math.PI:D>0&&s===0&&(D-=2*Math.PI);var P=Math.ceil(Math.abs(D/(Math.PI*.5+.001))),H=[];for(var B=0;B=r&&(r=e[n][t]);else while(n--)e[n]>=r&&(r=e[n]);return r}function r(e,t){if(!e||e.length===0)return undefined;var n=e.length-1,r=t?e[n][t]:e[n];if(t)while(n--)e[n][t]>>0;if(n===0)return-1;var r=0;arguments.length>0&&(r=Number(arguments[1]),r!==r?r=0:r!==0&&r!==Number.POSITIVE_INFINITY&&r!==Number.NEGATIVE_INFINITY&&(r=(r>0||-1)*Math.floor(Math.abs(r))));if(r>=n)return-1;var i=r>=0?r:Math.max(n-Math.abs(r),0);for(;i>>0;n>>0;r>>0;n>>0;n>>0;i>>0,n=0,r;if(arguments.length>1)r=arguments[1];else do{if(n in this){r=this[n++];break}if(++n>=t)throw new TypeError}while(!0);for(;n/g,">")}String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\xA0]+/,"").replace(/[\s\xA0]+$/,"")}),fabric.util.string={camelize:e,capitalize:t,escapeXml:n}}(),function(){var e=Array.prototype.slice,t=Function.prototype.apply,n=function(){};Function.prototype.bind||(Function.prototype.bind=function(r){var i=this,s=e.call(arguments,1),o;return s.length?o=function(){return t.call(i,this instanceof n?this:r,s.concat(e.call(arguments)))}:o=function(){return t.call(i,this instanceof n?this:r,arguments)},n.prototype=this.prototype,o.prototype=new n,o})}(),function(){function i(){}function s(t){var n=this.constructor.superclass.prototype[t];return arguments.length>1?n.apply(this,e.call(arguments,1)):n.call(this)}function o(){function u(){this.initialize.apply(this,arguments)}var n=null,o=e.call(arguments,0);typeof o[0]=="function"&&(n=o.shift()),u.superclass=n,u.subclasses=[],n&&(i.prototype=n.prototype,u.prototype=new i,n.subclasses.push(u));for(var a=0,f=o.length;a-1?e.prototype[i]=function(e){return function(){var n=this.constructor.superclass;this.constructor.superclass=r;var i=t[e].apply(this,arguments);this.constructor.superclass=n;if(e!=="initialize")return i}}(i):e.prototype[i]=t[i],n&&(t.toString!==Object.prototype.toString&&(e.prototype.toString=t.toString),t.valueOf!==Object.prototype.valueOf&&(e.prototype.valueOf=t.valueOf))};fabric.util.createClass=o}(),function(){function e(e){var t=Array.prototype.slice.call(arguments,1),n,r,i=t.length;for(r=0;r-1?s(e,t.match(/opacity:\s*(\d?\.?\d*)/)[1]):e;for(var r in t)if(r==="opacity")s(e,t[r]);else{var i=r==="float"||r==="cssFloat"?typeof n.styleFloat=="undefined"?"cssFloat":"styleFloat":r;n[i]=t[r]}return e}var t=fabric.document.createElement("div"),n=typeof t.style.opacity=="string",r=typeof t.style.filter=="string",i=/alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,s=function(e){return e};n?s=function(e,t){return e.style.opacity=t,e}:r&&(s=function(e,t){var n=e.style;return e.currentStyle&&!e.currentStyle.hasLayout&&(n.zoom=1),i.test(n.filter)?(t=t>=.9999?"":"alpha(opacity="+t*100+")",n.filter=n.filter.replace(i,t)):n.filter+=" alpha(opacity="+t*100+")",e}),fabric.util.setStyle=e}(),function(){function t(e){return typeof e=="string"?fabric.document.getElementById(e):e}function s(e,t){var n=fabric.document.createElement(e);for(var r in t)r==="class"?n.className=t[r]:r==="for"?n.htmlFor=t[r]:n.setAttribute(r,t[r]);return n}function o(e,t){(" "+e.className+" ").indexOf(" "+t+" ")===-1&&(e.className+=(e.className?" ":"")+t)}function u(e,t,n){return typeof t=="string"&&(t=s(t,n)),e.parentNode&&e.parentNode.replaceChild(t,e),t.appendChild(e),t}function a(e){var t,n,r={left:0,top:0},i=e&&e.ownerDocument,s={left:0,top:0},o={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!i)return{left:0,top:0};for(var u in o)s[o[u]]+=parseInt(f(e,u),10)||0;return t=i.documentElement,typeof e.getBoundingClientRect!="undefined"&&(r=e.getBoundingClientRect()),i!=null&&i===i.window?n=i:n=i.nodeType===9&&(i.defaultView||i.parentWindow),{left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)+s.left,top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0)+s.top}}function f(e,t){e.style||(e.style={});if(fabric.document.defaultView&&fabric.document.defaultView.getComputedStyle)return fabric.document.defaultView.getComputedStyle(e,null)[t];var n=e.style[t];return!n&&e.currentStyle&&(n=e.currentStyle[t]),n}var e=Array.prototype.slice,n=function(t){return e.call(t,0)},r;try{r=n(fabric.document.childNodes)instanceof Array}catch(i){}r||(n=function(e){var t=new Array(e.length),n=e.length;while(n--)t[n]=e[n];return t}),function(){function n(e){return typeof e.onselectstart!="undefined"&&(e.onselectstart=fabric.util.falseFunction),t?e.style[t]="none":typeof e.unselectable=="string"&&(e.unselectable="on"),e}function r(e){return typeof e.onselectstart!="undefined"&&(e.onselectstart=null),t?e.style[t]="":typeof e.unselectable=="string"&&(e.unselectable=""),e}var e=fabric.document.documentElement.style,t="userSelect"in e?"userSelect":"MozUserSelect"in e?"MozUserSelect":"WebkitUserSelect"in e?"WebkitUserSelect":"KhtmlUserSelect"in e?"KhtmlUserSelect":"";fabric.util.makeElementUnselectable=n,fabric.util.makeElementSelectable=r}(),function(){function e(e,t){var n=fabric.document.getElementsByTagName("head")[0],r=fabric.document.createElement("script"),i=!0;r.onload=r.onreadystatechange=function(e){if(i){if(typeof this.readyState=="string"&&this.readyState!=="loaded"&&this.readyState!=="complete")return;i=!1,t(e||fabric.window.event),r=r.onload=r.onreadystatechange=null}},r.src=e,n.appendChild(r)}fabric.util.getScript=e}(),fabric.util.getById=t,fabric.util.toArray=n,fabric.util.makeElement=s,fabric.util.addClass=o,fabric.util.wrapElement=u,fabric.util.getElementOffset=a,fabric.util.getElementStyle=f}(),function(){function e(e,t){return e+(/\?/.test(e)?"&":"?")+t}function n(){}function r(r,i){i||(i={});var s=i.method?i.method.toUpperCase():"GET",o=i.onComplete||function(){},u=t(),a;return u.onreadystatechange=function(){u.readyState===4&&(o(u),u.onreadystatechange=n)},s==="GET"&&(a=null,typeof i.parameters=="string"&&(r=e(r,i.parameters))),u.open(s,r,!0),(s==="POST"||s==="PUT")&&u.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),u.send(a),u}var t=function(){var e=[function(){return new ActiveXObject("Microsoft.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP.3.0")},function(){return new XMLHttpRequest}];for(var t=e.length;t--;)try{var n=e[t]();if(n)return e[t]}catch(r){}}();fabric.util.request=r}(),function(){function e(e){e||(e={});var t=+(new Date),r=e.duration||500,i=t+r,s,o=e.onChange||function(){},u=e.abort||function(){return!1},a=e.easing||function(e,t,n,r){return-n*Math.cos(e/r*(Math.PI/2))+n+t},f="startValue"in e?e.startValue:0,l="endValue"in e?e.endValue:100,c=e.byValue||l-f;e.onStart&&e.onStart(),function h(){s=+(new Date);var l=s>i?r:s-t;if(u()){e.onComplete&&e.onComplete();return}o(a(l,f,c,r));if(s>i){e.onComplete&&e.onComplete();return}n(h)}()}var t=fabric.window.requestAnimationFrame||fabric.window.webkitRequestAnimationFrame||fabric.window.mozRequestAnimationFrame||fabric.window.oRequestAnimationFrame||fabric.window.msRequestAnimationFrame||function(e){fabric.window.setTimeout(e,1e3/60)},n=function(){return t.apply(fabric.window,arguments)};fabric.util.animate=e,fabric.util.requestAnimFrame=n}(),function(){function e(e,t,n,r){return n*(e/=r)*e+t}function t(e,t,n,r){return-n*(e/=r)*(e-2)+t}function n(e,t,n,r){return e/=r/2,e<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}function r(e,t,n,r){return n*(e/=r)*e*e+t}function i(e,t,n,r){return n*((e=e/r-1)*e*e+1)+t}function s(e,t,n,r){return e/=r/2,e<1?n/2*e*e*e+t:n/2*((e-=2)*e*e+2)+t}function o(e,t,n,r){return n*(e/=r)*e*e*e+t}function u(e,t,n,r){return-n*((e=e/r-1)*e*e*e-1)+t}function a(e,t,n,r){return e/=r/2,e<1?n/2*e*e*e*e+t:-n/2*((e-=2)*e*e*e-2)+t}function f(e,t,n,r){return n*(e/=r)*e*e*e*e+t}function l(e,t,n,r){return n*((e=e/r-1)*e*e*e*e+1)+t}function c(e,t,n,r){return e/=r/2,e<1?n/2*e*e*e*e*e+t:n/2*((e-=2)*e*e*e*e+2)+t}function h(e,t,n,r){return-n*Math.cos(e/r*(Math.PI/2))+n+t}function p(e,t,n,r){return n*Math.sin(e/r*(Math.PI/2))+t}function d(e,t,n,r){return-n/2*(Math.cos(Math.PI*e/r)-1)+t}function v(e,t,n,r){return e===0?t:n*Math.pow(2,10*(e/r-1))+t}function m(e,t,n,r){return e===r?t+n:n*(-Math.pow(2,-10*e/r)+1)+t}function g(e,t,n,r){return e===0?t:e===r?t+n:(e/=r/2,e<1?n/2*Math.pow(2,10*(e-1))+t:n/2*(-Math.pow(2,-10*--e)+2)+t)}function y(e,t,n,r){return-n*(Math.sqrt(1-(e/=r)*e)-1)+t}function b(e,t,n,r){return n*Math.sqrt(1-(e=e/r-1)*e)+t}function w(e,t,n,r){return e/=r/2,e<1?-n/2*(Math.sqrt(1-e*e)-1)+t:n/2*(Math.sqrt(1-(e-=2)*e)+1)+t}function E(e,t,n,r){var i=1.70158,s=0,o=n;return e===0?t:(e/=r,e===1?t+n:(s||(s=r*.3),o-1;e=e.split(/\s+/);var n=[],r,i;if(t){r=0,i=e.length;for(;r/i,"")));if(!s.documentElement)return;t.parseSVGDocument(s.documentElement,function(r,i){w.set(e,{objects:t.util.array.invoke(r,"toObject"),options:i}),n(r,i)},r)}e=e.replace(/^\n\s*/,"").trim(),w.has(e,function(r){r?w.get(e,function(e){var t=S(e);n(t.objects,t.options)}):new t.util.request(e,{method:"get",onComplete:i})})}function S(e){var n=e.objects,i=e.options;return n=n.map(function(e){return t[r(e.type)].fromObject(e)}),{objects:n,options:i}}function x(e,n,r){e=e.trim();var i;if(typeof DOMParser!="undefined"){var s=new DOMParser;s&&s.parseFromString&&(i=s.parseFromString(e,"text/xml"))}else t.window.ActiveXObject&&(i=new ActiveXObject("Microsoft.XMLDOM"),i.async="false",i.loadXML(e.replace(//i,"")));t.parseSVGDocument(i.documentElement,function(e,t){n(e,t)},r)}function T(e){var t="";for(var n=0,r=e.length;n',"",""].join("")),t}function N(e){var t="";return e.backgroundColor&&e.backgroundColor.source&&(t=['',''].join("")),t}function C(e){var t=e.getElementsByTagName("linearGradient"),n=e.getElementsByTagName("radialGradient"),r,i,s={};i=t.length;for(;i--;)r=t[i],s[r.getAttribute("id")]=r;i=n.length;for(;i--;)r=n[i],s[r.getAttribute("id")]=r;return s}var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.string.capitalize,i=t.util.object.clone,s=t.util.toFixed,o=t.util.multiplyTransformMatrices;t.SHARED_ATTRIBUTES=["transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width"];var u={"fill-opacity":"fillOpacity","fill-rule":"fillRule","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight",cx:"left",x:"left",r:"radius","stroke-dasharray":"strokeDashArray","stroke-linecap":"strokeLineCap","stroke-linejoin":"strokeLineJoin","stroke-miterlimit":"strokeMiterLimit","stroke-opacity":"strokeOpacity","stroke-width":"strokeWidth","text-decoration":"textDecoration",cy:"top",y:"top",transform:"transformMatrix"},a={stroke:"strokeOpacity",fill:"fillOpacity"};t.parseTransformAttribute=function(){function e(e,t){var n=t[0];e[0]=Math.cos(n),e[1]=Math.sin(n),e[2]=-Math.sin(n),e[3]=Math.cos(n)}function n(e,t){var n=t[0],r=t.length===2?t[1]:t[0];e[0]=n,e[3]=r}function r(e,t){e[2]=t[0]}function i(e,t){e[1]=t[0]}function s(e,t){e[4]=t[0],t.length===2&&(e[5]=t[1])}var o=[1,0,0,1,0,0],u="(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)",a="(?:\\s+,?\\s*|,\\s*)",f="(?:(skewX)\\s*\\(\\s*("+u+")\\s*\\))",l="(?:(skewY)\\s*\\(\\s*("+u+")\\s*\\))",c="(?:(rotate)\\s*\\(\\s*("+u+")(?:"+a+"("+u+")"+a+"("+u+"))?\\s*\\))",h="(?:(scale)\\s*\\(\\s*("+u+")(?:"+a+"("+u+"))?\\s*\\))",p="(?:(translate)\\s*\\(\\s*("+u+")(?:"+a+"("+u+"))?\\s*\\))",d="(?:(matrix)\\s*\\(\\s*("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+"\\s*\\))",v="(?:"+d+"|"+p+"|"+h+"|"+c+"|"+f+"|"+l+")",m="(?:"+v+"(?:"+a+v+")*"+")",g="^\\s*(?:"+m+"?)\\s*$",y=new RegExp(g),b=new RegExp(v,"g");return function(u){var a=o.concat(),f=[];if(!u||u&&!y.test(u))return a;u.replace(b,function(t){var u=(new RegExp(v)).exec(t).filter(function(e){return e!==""&&e!=null}),l=u[1],c=u.slice(2).map(parseFloat);switch(l){case"translate":s(a,c);break;case"rotate":e(a,c);break;case"scale":n(a,c);break;case"skewX":r(a,c);break;case"skewY":i(a,c);break;case"matrix":a=c}f.push(a.concat()),a=o.concat()});var l=f[0];while(f.length>1)f.shift(),l=t.util.multiplyTransformMatrices(l,f[0]);return l}}(),t.parseSVGDocument=function(){function s(e,t){while(e&&(e=e.parentNode))if(t.test(e.nodeName))return!0;return!1}var e=/^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/,n="(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)",r=new RegExp("^\\s*("+n+"+)\\s*,?"+"\\s*("+n+"+)\\s*,?"+"\\s*("+n+"+)\\s*,?"+"\\s*("+n+"+)\\s*"+"$");return function(n,o,u){if(!n)return;var a=new Date,f=t.util.toArray(n.getElementsByTagName("*"));if(f.length===0){f=n.selectNodes("//*[name(.)!='svg']");var l=[];for(var c=0,h=f.length;ce.x&&this.y>e.y},gte:function(e){return this.x>=e.x&&this.y>=e.y},lerp:function(e,t){return new n(this.x+(e.x-this.x)*t,this.y+(e.y-this.y)*t)},distanceFrom:function(e){var t=this.x-e.x,n=this.y-e.y;return Math.sqrt(t*t+n*n)},midPointFrom:function(e){return new n(this.x+(e.x-this.x)/2,this.y+(e.y-this.y)/2)},min:function(e){return new n(Math.min(this.x,e.x),Math.min(this.y,e.y))},max:function(e){return new n(Math.max(this.x,e.x),Math.max(this.y,e.y))},toString:function(){return this.x+","+this.y},setXY:function(e,t){this.x=e,this.y=t},setFromPoint:function(e){this.x=e.x,this.y=e.y},swap:function(e){var t=this.x,n=this.y;this.x=e.x,this.y=e.y,e.x=t,e.y=n}}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function n(e){this.status=e,this.points=[]}var t=e.fabric||(e.fabric={});if(t.Intersection){t.warn("fabric.Intersection is already defined");return}t.Intersection=n,t.Intersection.prototype={appendPoint:function(e){this.points.push(e)},appendPoints:function(e){this.points=this.points.concat(e)}},t.Intersection.intersectLineLine=function(e,r,i,s){var o,u=(s.x-i.x)*(e.y-i.y)-(s.y-i.y)*(e.x-i.x),a=(r.x-e.x)*(e.y-i.y)-(r.y-e.y)*(e.x-i.x),f=(s.y-i.y)*(r.x-e.x)-(s.x-i.x)*(r.y-e.y);if(f!==0){var l=u/f,c=a/f;0<=l&&l<=1&&0<=c&&c<=1?(o=new n("Intersection"),o.points.push(new t.Point(e.x+l*(r.x-e.x),e.y+l*(r.y-e.y)))):o=new n}else u===0||a===0?o=new n("Coincident"):o=new n("Parallel");return o},t.Intersection.intersectLinePolygon=function(e,t,r){var i=new n,s=r.length;for(var o=0;o0&&(i.status="Intersection"),i},t.Intersection.intersectPolygonPolygon=function(e,t){var r=new n,i=e.length;for(var s=0;s0&&(r.status="Intersection"),r},t.Intersection.intersectPolygonRectangle=function(e,r,i){var s=r.min(i),o=r.max(i),u=new t.Point(o.x,s.y),a=new t.Point(s.x,o.y),f=n.intersectLinePolygon(s,u,e),l=n.intersectLinePolygon(u,o,e),c=n.intersectLinePolygon(o,a,e),h=n.intersectLinePolygon(a,s,e),p=new n;return p.appendPoints(f.points),p.appendPoints(l.points),p.appendPoints(c.points),p.appendPoints(h.points),p.points.length>0&&(p.status="Intersection"),p}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function n(e){e?this._tryParsingColor(e):this.setSource([0,0,0,1])}function r(e,t,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?e+(t-e)*6*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e}var t=e.fabric||(e.fabric={});if(t.Color){t.warn("fabric.Color is already defined.");return}t.Color=n,t.Color.prototype={_tryParsingColor:function(e){var t;e in n.colorNameMap&&(e=n.colorNameMap[e]),t=n.sourceFromHex(e),t||(t=n.sourceFromRgb(e)),t||(t=n.sourceFromHsl(e)),t&&this.setSource(t)},_rgbToHsl:function(e,n,r){e/=255,n/=255,r/=255;var i,s,o,u=t.util.array.max([e,n,r]),a=t.util.array.min([e,n,r]);o=(u+a)/2;if(u===a)i=s=0;else{var f=u-a;s=o>.5?f/(2-u-a):f/(u+a);switch(u){case e:i=(n-r)/f+(n']:this.type==="radial"&&(i=["']);for(var s=0;s');return i.push(this.type==="linear"?"":""),i.join("")},toLive:function(e){var t;if(!this.type)return;this.type==="linear"?t=e.createLinearGradient(this.coords.x1,this.coords.y1,this.coords.x2,this.coords.y2):this.type==="radial"&&(t=e.createRadialGradient(this.coords.x1,this.coords.y1,this.coords.r1,this.coords.x2,this.coords.y2,this.coords.r2));for(var n=0,r=this.colorStops.length;n'+''+""},toLive:function(e){var t=typeof this.source=="function"?this.source():this.source;return e.createPattern(t,this.repeat)}}),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Shadow){t.warn("fabric.Shadow is already defined.");return}t.Shadow=t.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,initialize:function(e){typeof e=="string"&&(e=this._parseShadow(e));for(var n in e)this[n]=e[n];this.id=t.Object.__uid++},_parseShadow:function(e){var n=e.trim(),r=t.Shadow.reOffsetsAndBlur.exec(n)||[],i=n.replace(t.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)";return{color:i.trim(),offsetX:parseInt(r[1],10)||0,offsetY:parseInt(r[2],10)||0,blur:parseInt(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(e){var t="SourceAlpha";return e&&(e.fill===this.color||e.stroke===this.color)&&(t="SourceGraphic"),''+''+''+""+""+''+""+"" -},toObject:function(){return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY}}}),t.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/}(typeof exports!="undefined"?exports:this),function(){"use strict";if(fabric.StaticCanvas){fabric.warn("fabric.StaticCanvas is already defined.");return}var e=fabric.util.object.extend,t=fabric.util.getElementOffset,n=fabric.util.removeFromArray,r=fabric.util.removeListener,i=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass({initialize:function(e,t){t||(t={}),this._initStatic(e,t),fabric.StaticCanvas.activeInstance=this},backgroundColor:"",backgroundImage:"",backgroundImageOpacity:1,backgroundImageStretch:!0,overlayImage:"",overlayImageLeft:0,overlayImageTop:0,includeDefaultValues:!0,stateful:!0,renderOnAddRemove:!0,clipTo:null,controlsAboveOverlay:!1,allowTouchScrolling:!1,onBeforeScaleRotate:function(){},_initStatic:function(e,t){this._objects=[],this._createLowerCanvas(e),this._initOptions(t),t.overlayImage&&this.setOverlayImage(t.overlayImage,this.renderAll.bind(this)),t.backgroundImage&&this.setBackgroundImage(t.backgroundImage,this.renderAll.bind(this)),t.backgroundColor&&this.setBackgroundColor(t.backgroundColor,this.renderAll.bind(this)),this.calcOffset()},calcOffset:function(){return this._offset=t(this.lowerCanvasEl),this},setOverlayImage:function(e,t,n){return fabric.util.loadImage(e,function(e){this.overlayImage=e,n&&"overlayImageLeft"in n&&(this.overlayImageLeft=n.overlayImageLeft),n&&"overlayImageTop"in n&&(this.overlayImageTop=n.overlayImageTop),t&&t()},this),this},setBackgroundImage:function(e,t,n){return fabric.util.loadImage(e,function(e){this.backgroundImage=e,n&&"backgroundImageOpacity"in n&&(this.backgroundImageOpacity=n.backgroundImageOpacity),n&&"backgroundImageStretch"in n&&(this.backgroundImageStretch=n.backgroundImageStretch),t&&t()},this),this},setBackgroundColor:function(e,t){if(e.source){var n=this;fabric.util.loadImage(e.source,function(r){n.backgroundColor=new fabric.Pattern({source:r,repeat:e.repeat}),t&&t()})}else this.backgroundColor=e,t&&t();return this},_createCanvasElement:function(){var e=fabric.document.createElement("canvas");e.style||(e.style={});if(!e)throw i;return this._initCanvasElement(e),e},_initCanvasElement:function(e){fabric.util.createCanvasElement(e);if(typeof e.getContext=="undefined")throw i},_initOptions:function(e){for(var t in e)this[t]=e[t];this.width=parseInt(this.lowerCanvasEl.width,10)||0,this.height=parseInt(this.lowerCanvasEl.height,10)||0;if(!this.lowerCanvasEl.style)return;this.lowerCanvasEl.style.width=this.width+"px",this.lowerCanvasEl.style.height=this.height+"px"},_createLowerCanvas:function(e){this.lowerCanvasEl=fabric.util.getById(e)||this._createCanvasElement(),this._initCanvasElement(this.lowerCanvasEl),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(e){return this._setDimension("width",e)},setHeight:function(e){return this._setDimension("height",e)},setDimensions:function(e){for(var t in e)this._setDimension(t,e[t]);return this},_setDimension:function(e,t){return this.lowerCanvasEl[e]=t,this.lowerCanvasEl.style[e]=t+"px",this.upperCanvasEl&&(this.upperCanvasEl[e]=t,this.upperCanvasEl.style[e]=t+"px"),this.cacheCanvasEl&&(this.cacheCanvasEl[e]=t),this.wrapperEl&&(this.wrapperEl.style[e]=t+"px"),this[e]=t,this.calcOffset(),this.renderAll(),this},getElement:function(){return this.lowerCanvasEl},getActiveObject:function(){return null},getActiveGroup:function(){return null},_draw:function(e,t){if(!t)return;if(this.controlsAboveOverlay){var n=t.hasBorders,r=t.hasControls;t.hasBorders=t.hasControls=!1,t.render(e),t.hasBorders=n,t.hasControls=r}else t.render(e)},_onObjectAdded:function(e){this.stateful&&e.setupState(),e.setCoords(),e.canvas=this,this.fire("object:added",{target:e}),e.fire("added")},_onObjectRemoved:function(e){this.fire("object:removed",{target:e}),e.fire("removed")},getObjects:function(){return this._objects},clearContext:function(e){return e.clearRect(0,0,this.width,this.height),this},getContext:function(){return this.contextContainer},clear:function(){return this._objects.length=0,this.discardActiveGroup&&this.discardActiveGroup(),this.discardActiveObject&&this.discardActiveObject(),this.clearContext(this.contextContainer),this.contextTop&&this.clearContext(this.contextTop),this.fire("canvas:cleared"),this.renderAll(),this},renderAll:function(e){var t=this[e===!0&&this.interactive?"contextTop":"contextContainer"];this.contextTop&&this.selection&&!this._groupSelector&&this.clearContext(this.contextTop),e||this.clearContext(t),this.fire("before:render"),this.clipTo&&fabric.util.clipContext(this,t),this.backgroundColor&&(t.fillStyle=this.backgroundColor.toLive?this.backgroundColor.toLive(t):this.backgroundColor,t.fillRect(this.backgroundColor.offsetX||0,this.backgroundColor.offsetY||0,this.width,this.height)),typeof this.backgroundImage=="object"&&this._drawBackroundImage(t);var n=this.getActiveGroup();for(var r=0,i=this._objects.length;r','\n'),n.push("',"Created with Fabric.js ",fabric.version,"","",fabric.createSVGFontFacesMarkup(this.getObjects()),fabric.createSVGRefElementsMarkup(this),""),this.backgroundColor&&this.backgroundColor.source&&n.push('"),this.backgroundImage&&n.push(''),this.overlayImage&&n.push('');var r=this.getActiveGroup();r&&this.discardActiveGroup();for(var i=0,s=this.getObjects(),o=s.length;i"),n.join("")},remove:function(e){return this.getActiveObject()===e&&(this.fire("before:selection:cleared",{target:e}),this.discardActiveObject(),this.fire("selection:cleared")),fabric.Collection.remove.call(this,e)},sendToBack:function(e){return n(this._objects,e),this._objects.unshift(e),this.renderAll&&this.renderAll()},bringToFront:function(e){return n(this._objects,e),this._objects.push(e),this.renderAll&&this.renderAll()},sendBackwards:function(e,t){var r=this._objects.indexOf(e);if(r!==0){var i;if(t){i=r;for(var s=r-1;s>=0;--s){var o=e.intersectsWithObject(this._objects[s])||e.isContainedWithinObject(this._objects[s])||this._objects[s].isContainedWithinObject(e);if(o){i=s;break}}}else i=r-1;n(this._objects,e),this._objects.splice(i,0,e),this.renderAll&&this.renderAll()}return this},bringForward:function(e,t){var r=this._objects.indexOf(e);if(r!==this._objects.length-1){var i;if(t){i=r;for(var s=r+1;s"}}),e(fabric.StaticCanvas.prototype,fabric.Observable),e(fabric.StaticCanvas.prototype,fabric.Collection),e(fabric.StaticCanvas.prototype,fabric.DataURLExporter),e(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(e){var t=fabric.util.createCanvasElement();if(!t||!t.getContext)return null;var n=t.getContext("2d");if(!n)return null;switch(e){case"getImageData":return typeof n.getImageData!="undefined";case"setLineDash":return typeof n.setLineDash!="undefined";case"toDataURL":return typeof t.toDataURL!="undefined";case"toDataURLWithQuality":try{return t.toDataURL("image/jpeg",0),!0}catch(r){}return!1;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",setShadow:function(e){return this.shadow=new fabric.Shadow(e),this},_setBrushStyles:function(){var e=this.canvas.contextTop;e.strokeStyle=this.color,e.lineWidth=this.width,e.lineCap=this.strokeLineCap,e.lineJoin=this.strokeLineJoin},_setShadow:function(){if(!this.shadow)return;var e=this.canvas.contextTop;e.shadowColor=this.shadow.color,e.shadowBlur=this.shadow.blur,e.shadowOffsetX=this.shadow.offsetX,e.shadowOffsetY=this.shadow.offsetY},_resetShadow:function(){var e=this.canvas.contextTop;e.shadowColor="",e.shadowBlur=e.shadowOffsetX=e.shadowOffsetY=0}}),function(){var e=fabric.util.array.min,t=fabric.util.array.max;fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{initialize:function(e){this.canvas=e,this._points=[]},onMouseDown:function(e){this._prepareForDrawing(e),this._captureDrawingPath(e),this._render()},onMouseMove:function(e){this._captureDrawingPath(e),this.canvas.clearContext(this.canvas.contextTop),this._render()},onMouseUp:function(){this._finalizeAndAddPath()},_prepareForDrawing:function(e){var t=new fabric.Point(e.x,e.y);this._reset(),this._addPoint(t),this.canvas.contextTop.moveTo(t.x,t.y)},_addPoint:function(e){this._points.push(e)},_reset:function(){this._points.length=0,this._setBrushStyles(),this._setShadow()},_captureDrawingPath:function(e){var t=new fabric.Point(e.x,e.y);this._addPoint(t)},_render:function(){var e=this.canvas.contextTop;e.beginPath();var t=this._points[0],n=this._points[1];this._points.length===2&&t.x===n.x&&t.y===n.y&&(t.x-=.5,n.x+=.5),e.moveTo(t.x,t.y);for(var r=1,i=this._points.length;r0&&(t>this.targetFindTolerance?t-=this.targetFindTolerance:t=0,n>this.targetFindTolerance?n-=this.targetFindTolerance:n=0);var o=!0,u=r.getImageData(t,n,this.targetFindTolerance*2||1,this.targetFindTolerance*2||1);for(var a=3,f=u.data.length;ao.padding?l.x<0?l.x+=o.padding:l.x-=o.padding:l.x=0,i(l.y)>o.padding?l.y<0?l.y+=o.padding:l.y-=o.padding:l.y=0;var c=o.scaleX,h=o.scaleY;if(n==="equally"&&!u&&!a){var p=l.y+l.x,d=(o.height+o.strokeWidth)*r.original.scaleY+(o.width+o.strokeWidth)*r.original.scaleX;c=r.original.scaleX*p/d,h=r.original.scaleY*p/d,o.set("scaleX",c),o.set("scaleY",h)}else n?n==="x"&&!o.get("lockUniScaling")?(c=l.x/(o.width+o.strokeWidth),u||o.set("scaleX",c)):n==="y"&&!o.get("lockUniScaling")&&(h=l.y/(o.height+o.strokeWidth),a||o.set("scaleY",h)):(c=l.x/(o.width+o.strokeWidth),h=l.y/(o.height+o.strokeWidth),u||o.set("scaleX",c),a||o.set("scaleY",h));c<0&&(r.originX==="left"?r.originX="right":r.originX==="right"&&(r.originX="left")),h<0&&(r.originY==="top"?r.originY="bottom":r.originY==="bottom"&&(r.originY="top")),o.setPositionByOrigin(f,r.originX,r.originY)},_rotateObject:function(e,t){var i=this._currentTransform,s=this._offset;if(i.target.get("lockRotation"))return;var o=r(i.ey-i.top-s.top,i.ex-i.left-s.left),u=r(t-i.top-s.top,e-i.left-s.left),a=n(u-o+i.theta);a<0&&(a=360+a),i.target.angle=a},_setCursor:function(e){this.upperCanvasEl.style.cursor=e},_resetObjectTransform:function(e){e.scaleX=1,e.scaleY=1,e.setAngle(0)},_drawSelection:function(){var e=this.contextTop,t=this._groupSelector,n=t.left,r=t.top,s=i(n),o=i(r);e.fillStyle=this.selectionColor,e.fillRect(t.ex-(n>0?0:-n),t.ey-(r>0?0:-r),s,o),e.lineWidth=this.selectionLineWidth,e.strokeStyle=this.selectionBorderColor;if(this.selectionDashArray.length>1){var a=t.ex+u-(n>0?0:s),f=t.ey+u-(r>0?0:o);e.beginPath(),fabric.util.drawDashedLine(e,a,f,a+s,f,this.selectionDashArray),fabric.util.drawDashedLine(e,a,f+o-1,a+s,f+o-1,this.selectionDashArray),fabric.util.drawDashedLine(e,a,f,a,f+o,this.selectionDashArray),fabric.util.drawDashedLine(e,a+s-1,f,a+s-1,f+o,this.selectionDashArray),e.closePath(),e.stroke()}else e.strokeRect(t.ex+u-(n>0?0:s),t.ey+u-(r>0?0:o),s,o)},_findSelectedObjects:function(e){var t=[],n=this._groupSelector.ex,r=this._groupSelector.ey,i=n+this._groupSelector.left,u=r+this._groupSelector.top,a,f=new fabric.Point(s(n,i),s(r,u)),l=new fabric.Point(o(n,i),o(r,u)),c=n===i&&r===u;for(var h=this._objects.length;h--;){a=this._objects[h];if(!a)continue;if(a.intersectsWithRect(f,l)||a.isContainedWithinRect(f,l)||a.containsPoint(f)||a.containsPoint(l))if(this.selection&&a.selectable){a.set("active",!0),t.push(a);if(c)break}}t.length===1?this.setActiveObject(t[0],e):t.length>1&&(t=new fabric.Group(t.reverse()),this.setActiveGroup(t),t.saveCoords(),this.fire("selection:created",{target:t}),this.renderAll())},findTarget:function(e,t){if(this.skipTargetFind)return;var n,r=this.getPointer(e);if(this.controlsAboveOverlay&&this.lastRenderedObjectWithControlsAboveOverlay&&this.lastRenderedObjectWithControlsAboveOverlay.visible&&this.containsPoint(e,this.lastRenderedObjectWithControlsAboveOverlay)&&this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e,this._offset))return n=this.lastRenderedObjectWithControlsAboveOverlay,n;var i=this.getActiveGroup();if(i&&!t&&this.containsPoint(e,i))return n=i,n;var s=[];for(var o=this._objects.length;o--;)if(this._objects[o]&&this._objects[o].visible&&this._objects[o].evented&&this.containsPoint(e,this._objects[o])){if(!this.perPixelTargetFind&&!this._objects[o].perPixelTargetFind){n=this._objects[o],this.relatedTarget=n;break}s[s.length]=this._objects[o]}for(var u=0,a=s.length;u"},get:function(e){return this[e]},set:function(e,t){if(typeof e=="object")for(var n in e)this._set(n,e[n]);else typeof t=="function"&&e!=="clipTo"?this._set(e,t(this.get(e))):this._set(e,t);return this},_set:function(e,n){var i=e==="scaleX"||e==="scaleY";return i&&(n=this._constrainScale(n)),e==="scaleX"&&n<0?(this.flipX=!this.flipX,n*=-1):e==="scaleY"&&n<0?(this.flipY=!this.flipY,n*=-1):e==="width"||e==="height"?this.minScaleLimit=r(Math.min(.1,1/Math.max(this.width,this.height)),2):e==="shadow"&&n&&!(n instanceof t.Shadow)&&(n=new t.Shadow(n)),this[e]=n,this},toggle:function(e){var t=this.get(e);return typeof t=="boolean"&&this.set(e,!t),this},setSourcePath:function(e){return this.sourcePath=e,this},render:function(e,n){if(this.width===0||this.height===0||!this.visible)return;e.save();var r=this.transformMatrix;r&&!this.group&&e.setTransform(r[0],r[1],r[2],r[3],r[4],r[5]),n||this.transform(e),this.stroke&&(e.lineWidth=this.strokeWidth,e.lineCap=this.strokeLineCap,e.lineJoin=this.strokeLineJoin,e.miterLimit=this.strokeMiterLimit,e.strokeStyle=this.stroke.toLive?this.stroke.toLive(e):this.stroke),this.fill&&(e.fillStyle=this.fill.toLive?this.fill.toLive(e):this.fill),r&&this.group&&(e.translate(-this.group.width/2,-this.group.height/2),e.transform(r[0],r[1],r[2],r[3],r[4],r[5])),this._setShadow(e),this.clipTo&&t.util.clipContext(this,e),this._render(e,n),this.clipTo&&e.restore(),this._removeShadow(e),this.active&&!n&&(this.drawBorders(e),this.drawControls(e)),e.restore()},_setShadow:function(e){if(!this.shadow)return;e.shadowColor=this.shadow.color,e.shadowBlur=this.shadow.blur,e.shadowOffsetX=this.shadow.offsetX,e.shadowOffsetY=this.shadow.offsetY},_removeShadow:function(e){e.shadowColor="",e.shadowBlur=e.shadowOffsetX=e.shadowOffsetY=0},_renderFill:function(e){if(!this.fill)return;this.fill.toLive&&(e.save(),e.translate(-this.width/2+this.fill.offsetX||0,-this.height/2+this.fill.offsetY||0)),e.fill(),this.fill.toLive&&e.restore(),this.shadow&&!this.shadow.affectStroke&&this._removeShadow(e)},_renderStroke:function(e){if(!this.stroke)return;e.save(),this.strokeDashArray?(1&this.strokeDashArray.length&&this.strokeDashArray.push.apply(this.strokeDashArray,this.strokeDashArray),o?(e.setLineDash(this.strokeDashArray),this._stroke&&this._stroke(e)):this._renderDashedStroke&&this._renderDashedStroke(e),e.stroke()):this._stroke?this._stroke(e):e.stroke(),this._removeShadow(e),e.restore()},clone:function(e,n){return this.constructor.fromObject?this.constructor.fromObject(this.toObject(n),e):new t.Object(this.toObject(n))},cloneAsImage:function(e){var n=this.toDataURL();return t.util.loadImage(n,function(n){e&&e(new t.Image(n))}),this},toDataURL:function(e){e||(e={});var n=t.util.createCanvasElement(),r=this.getBoundingRect();n.width=r.width,n.height=r.height,t.util.wrapElement(n,"div");var i=new t.Canvas(n);e.format==="jpg"&&(e.format="jpeg"),e.format==="jpeg"&&(i.backgroundColor="#fff");var s={active:this.get("active"),left:this.getLeft(),top:this.getTop()};this.set("active",!1),this.setPositionByOrigin(new t.Point(n.width/2,n.height/2),"center","center"),i.add(this);var o=i.toDataURL(e);return this.set(s).setCoords(),i.dispose(),i=null,o},isType:function(e){return this.type===e},complexity:function(){return 0},toJSON:function(e){return this.toObject(e)},setGradient:function(e,n){n||(n={});var r={colorStops:[]};r.type=n.type||(n.r1||n.r2?"radial":"linear"),r.coords={x1:n.x1,y1:n.y1,x2:n.x2,y2:n.y2};if(n.r1||n.r2)r.coords.r1=n.r1,r.coords.r2=n.r2;for(var i in n.colorStops){var s=new t.Color(n.colorStops[i]);r.colorStops.push({offset:i,color:s.toRgb(),opacity:s.getAlpha()})}return this.set(e,t.Gradient.forObject(this,r))},setPatternFill:function(e){return this.set("fill",new t.Pattern(e))},setShadow:function(e){return this.set("shadow",new t.Shadow(e))},setColor:function(e){return this.set("fill",e),this},centerH:function(){return this.canvas.centerObjectH(this),this},centerV:function(){return this.canvas.centerObjectV(this),this},center:function(){return this.centerH().centerV()},remove:function(){return this.canvas.remove(this)},sendToBack:function(){return this.group?t.StaticCanvas.prototype.sendToBack.call(this.group,this):this.canvas.sendToBack(this),this},bringToFront:function(){return this.group?t.StaticCanvas.prototype.bringToFront.call(this.group,this):this.canvas.bringToFront(this),this},sendBackwards:function(e){return this.group?t.StaticCanvas.prototype.sendBackwards.call(this.group,this,e):this.canvas.sendBackwards(this,e),this},bringForward:function(e){return this.group?t.StaticCanvas.prototype.bringForward.call(this.group,this,e):this.canvas.bringForward(this,e),this},moveTo:function(e){return this.group?t.StaticCanvas.prototype.moveTo.call(this.group,this,e):this.canvas.moveTo(this,e),this}}),t.util.createAccessors(t.Object),t.Object.prototype.rotate=t.Object.prototype.setAngle,n(t.Object.prototype,t.Observable),t.Object.NUM_FRACTION_DIGITS=2,t.Object.__uid=0}(typeof exports!="undefined"?exports:this),function(){var e=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{translateToCenterPoint:function(t,n,r){var i=t.x,s=t.y;return n==="left"?i=t.x+(this.getWidth()+this.strokeWidth*this.scaleX)/2:n==="right"&&(i=t.x-(this.getWidth()+this.strokeWidth*this.scaleX)/2),r==="top"?s=t.y+(this.getHeight()+this.strokeWidth*this.scaleY)/2:r==="bottom"&&(s=t.y-(this.getHeight()+this.strokeWidth*this.scaleY)/2),fabric.util.rotatePoint(new fabric.Point(i,s),t,e(this.angle))},translateToOriginPoint:function(t,n,r){var i=t.x,s=t.y;return n==="left"?i=t.x-(this.getWidth()+this.strokeWidth*this.scaleX)/2:n==="right"&&(i=t.x+(this.getWidth()+this.strokeWidth*this.scaleX)/2),r==="top"?s=t.y-(this.getHeight()+this.strokeWidth*this.scaleY)/2:r==="bottom"&&(s=t.y+(this.getHeight()+this.strokeWidth*this.scaleY)/2),fabric.util.rotatePoint(new fabric.Point(i,s),t,e(this.angle))},getCenterPoint:function(){var e=new fabric.Point(this.left,this.top);return this.translateToCenterPoint(e,this.originX,this.originY)},getPointByOrigin:function(e,t){var n=this.getCenterPoint();return this.translateToOriginPoint(n,e,t)},toLocalPoint:function(t,n,r){var i=this.getCenterPoint(),s,o;return n!==undefined&&r!==undefined?(n==="left"?s=i.x-(this.getWidth()+this.strokeWidth*this.scaleX)/2:n==="right"?s=i.x+(this.getWidth()+this.strokeWidth*this.scaleX)/2:s=i.x,r==="top"?o=i.y-(this.getHeight()+this.strokeWidth*this.scaleY)/2:r==="bottom"?o=i.y+(this.getHeight()+this.strokeWidth*this.scaleY)/2:o=i.y):(s=this.left,o=this.top),fabric.util.rotatePoint(new fabric.Point(t.x,t.y),i,-e(this.angle)).subtractEquals(new fabric.Point(s,o))},setPositionByOrigin:function(e,t,n){var r=this.translateToCenterPoint(e,t,n),i=this.translateToOriginPoint(r,this.originX,this.originY);this.set("left",i.x),this.set("top",i.y)},adjustPosition:function(t){var n=e(this.angle),r=this.getWidth()/2,i=Math.cos(n)*r,s=Math.sin(n)*r,o=this.getWidth(),u=Math.cos(n)*o,a=Math.sin(n)*o;this.originX==="center"&&t==="left"||this.originX==="right"&&t==="center"?(this.left-=i,this.top-=s):this.originX==="left"&&t==="center"||this.originX==="center"&&t==="right"?(this.left+=i,this.top+=s):this.originX==="left"&&t==="right"?(this.left+=u,this.top+=a):this.originX==="right"&&t==="left"&&(this.left-=u,this.top-=a),this.setCoords(),this.originX=t},_getLeftTopCoords:function(){return this.translateToOriginPoint(this.getCenterPoint(),"left","center")}})}(),function(){var e=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{oCoords:null,intersectsWithRect:function(e,t){var n=this.oCoords,r=new fabric.Point(n.tl.x,n.tl.y),i=new fabric.Point(n.tr.x,n.tr.y),s=new fabric.Point(n.bl.x,n.bl.y),o=new fabric.Point(n.br.x,n.br.y),u=fabric.Intersection.intersectPolygonRectangle([r,i,o,s],e,t);return u.status==="Intersection"},intersectsWithObject:function(e){function t(e){return{tl:new fabric.Point(e.tl.x,e.tl.y),tr:new fabric.Point(e.tr.x,e.tr.y),bl:new fabric.Point(e.bl.x,e.bl.y),br:new fabric.Point(e.br.x,e.br.y)}}var n=t(this.oCoords),r=t(e.oCoords),i=fabric.Intersection.intersectPolygonPolygon([n.tl,n.tr,n.br,n.bl],[r.tl,r.tr,r.br,r.bl]);return i.status==="Intersection"},isContainedWithinObject:function(e){var t=e.getBoundingRect(),n=new fabric.Point(t.left,t.top),r=new fabric.Point(t.left+t.width,t.top+t.height);return this.isContainedWithinRect(n,r)},isContainedWithinRect:function(e,t){var n=this.getBoundingRect();return n.left>e.x&&n.left+n.widthe.y&&n.top+n.height=e.y&&f.d.y>=e.y)continue;f.o.x===f.d.x&&f.o.x>=e.x?(o=f.o.x,u=e.y):(n=0,r=(f.d.y-f.o.y)/(f.d.x-f.o.x),i=e.y-n*e.x,s=f.o.y-r*f.o.x,o=-(i-s)/(n-r),u=i+n*o),o>=e.x&&(a+=1);if(a===2)break}return a},getBoundingRectWidth:function(){return this.getBoundingRect().width},getBoundingRectHeight:function(){return this.getBoundingRect().height},getBoundingRect:function(){this.oCoords||this.setCoords();var e=[this.oCoords.tl.x,this.oCoords.tr.x,this.oCoords.br.x,this.oCoords.bl.x],t=fabric.util.array.min(e),n=fabric.util.array.max(e),r=Math.abs(t-n),i=[this.oCoords.tl.y,this.oCoords.tr.y,this.oCoords.br.y,this.oCoords.bl.y],s=fabric.util.array.min(i),o=fabric.util.array.max(i),u=Math.abs(s-o);return{left:t,top:s,width:r,height:u}},getWidth:function(){return this.width*this.scaleX},getHeight:function(){return this.height*this.scaleY},_constrainScale:function(e){return Math.abs(e)1?this.strokeWidth:0,n=this.padding,r=e(this.angle);this.currentWidth=(this.width+t)*this.scaleX+n*2,this.currentHeight=(this.height+t)*this.scaleY+n*2,this.currentWidth<0&&(this.currentWidth=Math.abs(this.currentWidth));var i=Math.sqrt(Math.pow(this.currentWidth/2,2)+Math.pow(this.currentHeight/2,2)),s=Math.atan(isFinite(this.currentHeight/this.currentWidth)?this.currentHeight/this.currentWidth:0),o=Math.cos(s+r)*i,u=Math.sin(s+r)*i,a=Math.sin(r),f=Math.cos(r),l=this.getCenterPoint(),c={x:l.x-o,y:l.y-u},h={x:c.x+this.currentWidth*f,y:c.y+this.currentWidth*a},p={x:h.x-this.currentHeight*a,y:h.y+this.currentHeight*f},d={x:c.x-this.currentHeight*a,y:c.y+this.currentHeight*f},v={x:c.x-this.currentHeight/2*a,y:c.y+this.currentHeight/2*f},m={x:c.x+this.currentWidth/2*f,y:c.y+this.currentWidth/2*a},g={x:h.x-this.currentHeight/2*a,y:h.y+this.currentHeight/2*f},y={x:d.x+this.currentWidth/2*f,y:d.y+this.currentWidth/2*a},b={x:m.x,y:m.y};return this.oCoords={tl:c,tr:h,br:p,bl:d,ml:v,mt:m,mr:g,mb:y,mtr:b},this._setCornerCoords&&this._setCornerCoords(),this}})}(),fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(){return this.stateProperties.some(function(e){return this.get(e)!==this.originalState[e]},this)},saveState:function(e){return this.stateProperties.forEach(function(e){this.originalState[e]=this.get(e)},this),e&&e.stateProperties&&e.stateProperties.forEach(function(e){this.originalState[e]=this.get(e)},this),this},setupState:function(){return this.originalState={},this.saveState(),this}}),function(){var e=fabric.util.getPointer,t=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{_findTargetCorner:function(t,n){if(!this.hasControls||!this.active)return!1;var r=e(t,this.canvas.upperCanvasEl),i=r.x-n.left,s=r.y-n.top,o,u;for(var a in this.oCoords){if(a==="mtr"&&!this.hasRotatingPoint)continue;if(!(!this.get("lockUniScaling")||a!=="mt"&&a!=="mr"&&a!=="mb"&&a!=="ml"))continue;u=this._getImageLines(this.oCoords[a].corner),o=this._findCrossPoints({x:i,y:s},u);if(o!==0&&o%2===1)return this.__corner=a,a}return!1},_setCornerCoords:function(){var e=this.oCoords,n=t(this.angle),r=t(45-this.angle),i=Math.sqrt(2*Math.pow(this.cornerSize,2))/2,s=i*Math.cos(r),o=i*Math.sin(r),u=Math.sin(n),a=Math.cos(n);e.tl.corner={tl:{x:e.tl.x-o,y:e.tl.y-s},tr:{x:e.tl.x+s,y:e.tl.y-o},bl:{x:e.tl.x-s,y:e.tl.y+o},br:{x:e.tl.x+o,y:e.tl.y+s}},e.tr.corner={tl:{x:e.tr.x-o,y:e.tr.y-s},tr:{x:e.tr.x+s,y:e.tr.y-o},br:{x:e.tr.x+o,y:e.tr.y+s},bl:{x:e.tr.x-s,y:e.tr.y+o}},e.bl.corner={tl:{x:e.bl.x-o,y:e.bl.y-s},bl:{x:e.bl.x-s,y:e.bl.y+o},br:{x:e.bl.x+o,y:e.bl.y+s},tr:{x:e.bl.x+s,y:e.bl.y-o}},e.br.corner={tr:{x:e.br.x+s,y:e.br.y-o},bl:{x:e.br.x-s,y:e.br.y+o},br:{x:e.br.x+o,y:e.br.y+s},tl:{x:e.br.x-o,y:e.br.y-s}},e.ml.corner={tl:{x:e.ml.x-o,y:e.ml.y-s},tr:{x:e.ml.x+s,y:e.ml.y-o},bl:{x:e.ml.x-s,y:e.ml.y+o},br:{x:e.ml.x+o,y:e.ml.y+s}},e.mt.corner={tl:{x:e.mt.x-o,y:e.mt.y-s},tr:{x:e.mt.x+s,y:e.mt.y-o},bl:{x:e.mt.x-s,y:e.mt.y+o},br:{x:e.mt.x+o,y:e.mt.y+s}},e.mr.corner={tl:{x:e.mr.x-o,y:e.mr.y-s},tr:{x:e.mr.x+s,y:e.mr.y-o},bl:{x:e.mr.x-s,y:e.mr.y+o},br:{x:e.mr.x+o,y:e.mr.y+s}},e.mb.corner={tl:{x:e.mb.x-o,y:e.mb.y-s},tr:{x:e.mb.x+s,y:e.mb.y-o},bl:{x:e.mb.x-s,y:e.mb.y+o},br:{x:e.mb.x+o,y:e.mb.y+s}},e.mtr.corner={tl:{x:e.mtr.x-o+u*this.rotatingPointOffset,y:e.mtr.y-s-a*this.rotatingPointOffset},tr:{x:e.mtr.x+s+u*this.rotatingPointOffset,y:e.mtr.y-o-a*this.rotatingPointOffset},bl:{x:e.mtr.x-s+u*this.rotatingPointOffset,y:e.mtr.y+o-a*this.rotatingPointOffset},br:{x:e.mtr.x+o+u*this.rotatingPointOffset,y:e.mtr.y+s-a*this.rotatingPointOffset}}},drawBorders:function(e){if(!this.hasBorders)return this;var t=this.padding,n=t*2,r=~~(this.strokeWidth/2)*2;e.save(),e.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,e.strokeStyle=this.borderColor;var i=1/this._constrainScale(this.scaleX),s=1/this._constrainScale(this.scaleY);e.lineWidth=1/this.borderScaleFactor,e.scale(i,s);var o=this.getWidth(),u=this.getHeight();e.strokeRect(~~(-(o/2)-t-r/2*this.scaleX)-.5,~~(-(u/2)-t-r/2*this.scaleY)-.5,~~(o+n+r*this.scaleX)+1,~~(u+n+r*this.scaleY)+1);if(this.hasRotatingPoint&&!this.get("lockRotation")&&this.hasControls){var a=(this.flipY?u+r*this.scaleY+t*2:-u-r*this.scaleY-t*2)/2;e.beginPath(),e.moveTo(0,a),e.lineTo(0,a+(this.flipY?this.rotatingPointOffset:-this.rotatingPointOffset)),e.closePath(),e.stroke()}return e.restore(),this},drawControls:function(e){if(!this.hasControls)return this;var t=this.cornerSize,n=t/2,r=~~(this.strokeWidth/2),i=-(this.width/2),s=-(this.height/2),o,u,a=t/this.scaleX,f=t/this.scaleY,l=this.padding/this.scaleX,c=this.padding/this.scaleY,h=n/this.scaleY,p=n/this.scaleX,d=(n-t)/this.scaleX,v=(n-t)/this.scaleY,m=this.height,g=this.width,y=this.transparentCorners?"strokeRect":"fillRect",b=this.transparentCorners,w=typeof G_vmlCanvasManager!="undefined";return e.save(),e.lineWidth=1/Math.max(this.scaleX,this.scaleY),e.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,e.strokeStyle=e.fillStyle=this.cornerColor,o=i-p-r-l,u=s-h-r-c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),o=i+g-p+r+l,u=s-h-r-c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),o=i-p-r-l,u=s+m+v+r+c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),o=i+g+d+r+l,u=s+m+v+r+c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),this.get("lockUniScaling")||(o=i+g/2-p,u=s-h-r-c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),o=i+g/2-p,u=s+m+v+r+c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),o=i+g+d+r+l,u=s+m/2-h,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f),o=i-p-r-l,u=s+m/2-h,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f)),this.hasRotatingPoint&&(o=i+g/2-p,u=this.flipY?s+m+this.rotatingPointOffset/this.scaleY-f/2+r+c:s-this.rotatingPointOffset/this.scaleY-f/2-r-c,w||b||e.clearRect(o,u,a,f),e[y](o,u,a,f)),e.restore(),this}})}(),fabric.util.object.extend(fabric.StaticCanvas.prototype,{FX_DURATION:500,fxCenterObjectH:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("left"),endValue:this.getCenter().left,duration:this.FX_DURATION,onChange:function(t){e.set("left",t),s.renderAll(),i()},onComplete:function(){e.setCoords(),r()}}),this},fxCenterObjectV:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("top"),endValue:this.getCenter().top,duration:this.FX_DURATION,onChange:function(t){e.set("top",t),s.renderAll(),i()},onComplete:function(){e.setCoords(),r()}}),this},fxRemove:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("opacity"),endValue:0,duration:this.FX_DURATION,onStart:function(){e.set("active",!1)},onChange:function(t){e.set("opacity",t),s.renderAll(),i()},onComplete:function(){s.remove(e),r()}}),this}}),fabric.util.object.extend(fabric.Object.prototype,{animate:function(){if(arguments[0]&&typeof arguments[0]=="object"){var e=[],t,n;for(t in arguments[0])e.push(t);for(var r=0,i=e.length;r'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Line.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),t.Line.fromElement=function(e,r){var i=t.parseAttributes(e,t.Line.ATTRIBUTE_NAMES),s=[i.x1||0,i.y1||0,i.x2||0,i.y2||0];return new t.Line(s,n(i,r))},t.Line.fromObject=function(e){var n=[e.x1,e.y1,e.x2,e.y2];return new t.Line(n,e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function i(e){return"radius"in e&&e.radius>0}var t=e.fabric||(e.fabric={}),n=Math.PI*2,r=t.util.object.extend;if(t.Circle){t.warn("fabric.Circle is already defined.");return}t.Circle=t.util.createClass(t.Object,{type:"circle",initialize:function(e){e=e||{},this.set("radius",e.radius||0),this.callSuper("initialize",e)},_set:function(e,t){return this.callSuper("_set",e,t),e==="radius"&&this.setRadius(t),this},toObject:function(e){return r(this.callSuper("toObject",e),{radius:this.get("radius")})},toSVG:function(e){var t=this._createBaseSVGMarkup();return t.push("'),e?e(t.join("")):t.join("")},_render:function(e,t){e.beginPath(),e.globalAlpha=this.group?e.globalAlpha*this.opacity:this.opacity,e.arc(t?this.left:0,t?this.top:0,this.radius,0,n,!1),e.closePath(),this._renderFill(e),this._renderStroke(e)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(e){this.radius=e,this.set("width",e*2).set("height",e*2)},complexity:function(){return 1}}),t.Circle.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),t.Circle.fromElement=function(e,n){n||(n={});var s=t.parseAttributes(e,t.Circle.ATTRIBUTE_NAMES);if(!i(s))throw new Error("value of `r` attribute is required and can not be negative");"left"in s&&(s.left-=n.width/2||0),"top"in s&&(s.top-=n.height/2||0);var o=new t.Circle(r(s,n));return o.cx=parseFloat(e.getAttribute("cx"))||0,o.cy=parseFloat(e.getAttribute("cy"))||0,o},t.Circle.fromObject=function(e){return new t.Circle(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Triangle){t.warn("fabric.Triangle is already defined");return}t.Triangle=t.util.createClass(t.Object,{type:"triangle",initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("width",e.width||100).set("height",e.height||100)},_render:function(e){var t=this.width/2,n=this.height/2;e.beginPath(),e.moveTo(-t,n),e.lineTo(0,-n),e.lineTo(t,n),e.closePath(),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n=this.width/2,r=this.height/2;e.beginPath(),t.util.drawDashedLine(e,-n,r,0,-r,this.strokeDashArray),t.util.drawDashedLine(e,0,-r,n,r,this.strokeDashArray),t.util.drawDashedLine(e,n,r,-n,r,this.strokeDashArray),e.closePath()},toSVG:function(e){var t=this._createBaseSVGMarkup(),n=this.width/2,r=this.height/2,i=[-n+" "+r,"0 "+ -r,n+" "+r].join(",");return t.push("'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Triangle.fromObject=function(e){return new t.Triangle(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=Math.PI*2,r=t.util.object.extend;if(t.Ellipse){t.warn("fabric.Ellipse is already defined.");return}t.Ellipse=t.util.createClass(t.Object,{type:"ellipse",rx:0,ry:0,initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("rx",e.rx||0),this.set("ry",e.ry||0),this.set("width",this.get("rx")*2),this.set("height",this.get("ry")*2)},toObject:function(e){return r(this.callSuper("toObject",e),{rx:this.get("rx"),ry:this.get("ry")})},toSVG:function(e){var t=this._createBaseSVGMarkup();return t.push("'),e?e(t.join("")):t.join("")},render:function(e,t){if(this.rx===0||this.ry===0)return;return this.callSuper("render",e,t)},_render:function(e,t){e.beginPath(),e.save(),e.globalAlpha=this.group?e.globalAlpha*this.opacity:this.opacity,this.transformMatrix&&this.group&&e.translate(this.cx,this.cy),e.transform(1,0,0,this.ry/this.rx,0,0),e.arc(t?this.left:0,t?this.top:0,this.rx,0,n,!1),this._renderFill(e),this._renderStroke(e),e.restore()},complexity:function(){return 1}}),t.Ellipse.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),t.Ellipse.fromElement=function(e,n){n||(n={});var i=t.parseAttributes(e,t.Ellipse.ATTRIBUTE_NAMES),s=i.left,o=i.top;"left"in i&&(i.left-=n.width/2||0),"top"in i&&(i.top-=n.height/2||0);var u=new t.Ellipse(r(i,n));return u.cx=s||0,u.cy=o||0,u},t.Ellipse.fromObject=function(e){return new t.Ellipse(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function i(e){return e.left=e.left||0,e.top=e.top||0,e}var t=e.fabric||(e.fabric={}),n=t.util.object.extend;if(t.Rect){console.warn("fabric.Rect is already defined");return}var r=t.Object.prototype.stateProperties.concat();r.push("rx","ry","x","y"),t.Rect=t.util.createClass(t.Object,{stateProperties:r,type:"rect",rx:0,ry:0,x:0,y:0,strokeDashArray:null,initialize:function(e){e=e||{},this.callSuper("initialize",e),this._initRxRy(),this.x=e.x||0,this.y=e.y||0},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(e){var t=this.rx||0,n=this.ry||0,r=-this.width/2,i=-this.height/2,s=this.width,o=this.height,u=this.group&&this.group.type!=="group";e.beginPath(),e.globalAlpha=u?e.globalAlpha*this.opacity:this.opacity,this.transformMatrix&&u&&e.translate(this.width/2+this.x,this.height/2+this.y),!this.transformMatrix&&u&&e.translate(-this.group.width/2+this.width/2+this.x,-this.group.height/2+this.height/2+this.y);var a=t!==0||n!==0;e.moveTo(r+t,i),e.lineTo(r+s-t,i),a&&e.quadraticCurveTo(r+s,i,r+s,i+n,r+s,i+n),e.lineTo(r+s,i+o-n),a&&e.quadraticCurveTo(r+s,i+o,r+s-t,i+o,r+s-t,i+o),e.lineTo(r+t,i+o),a&&e.quadraticCurveTo(r,i+o,r,i+o-n,r,i+o-n),e.lineTo(r,i+n),a&&e.quadraticCurveTo(r,i,r+t,i,r+t,i),e.closePath(),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n=-this.width/2,r=-this.height/2,i=this.width,s=this.height;e.beginPath(),t.util.drawDashedLine(e,n,r,n+i,r,this.strokeDashArray),t.util.drawDashedLine(e,n+i,r,n+i,r+s,this.strokeDashArray),t.util.drawDashedLine(e,n+i,r+s,n,r+s,this.strokeDashArray),t.util.drawDashedLine(e,n,r+s,n,r,this.strokeDashArray),e.closePath()},_normalizeLeftTopProperties:function(e){return"left"in e&&this.set("left",e.left+this.getWidth()/2),this.set("x",e.left||0),"top"in e&&this.set("top",e.top+this.getHeight()/2),this.set("y",e.top||0),this},toObject:function(e){var t=n(this.callSuper("toObject",e),{rx:this.get("rx")||0,ry:this.get("ry")||0,x:this.get("x"),y:this.get("y")});return this.includeDefaultValues||this._removeDefaultValues(t),t},toSVG:function(e){var t=this._createBaseSVGMarkup();return t.push("'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Rect.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),t.Rect.fromElement=function(e,r){if(!e)return null;var s=t.parseAttributes(e,t.Rect.ATTRIBUTE_NAMES);s=i(s);var o=new t.Rect(n(r?t.util.object.clone(r):{},s));return o._normalizeLeftTopProperties(s),o},t.Rect.fromObject=function(e){return new t.Rect(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.toFixed,r=t.util.array.min;if(t.Polyline){t.warn("fabric.Polyline is already defined");return}t.Polyline=t.util.createClass(t.Object,{type:"polyline",initialize:function(e,t,n){t=t||{},this.set("points",e),this.callSuper("initialize",t),this._calcDimensions(n)},_calcDimensions:function(e){return t.Polygon.prototype._calcDimensions.call(this,e)},toObject:function(e){return t.Polygon.prototype.toObject.call(this,e)},toSVG:function(e){var t=[],r=this._createBaseSVGMarkup();for(var i=0,s=this.points.length;i'),e?e(r.join("")):r.join("")},_render:function(e){var t;e.beginPath(),e.moveTo(this.points[0].x,this.points[0].y);for(var n=0,r=this.points.length;n'),e?e(n.join("")):n.join("")},_render:function(e){var t;e.beginPath(),e.moveTo(this.points[0].x,this.points[0].y);for(var n=0,r=this.points.length;n"},toObject:function(e){var t=s(this.callSuper("toObject",e),{path:this.path,pathOffset:this.pathOffset});return this.sourcePath&&(t.sourcePath=this.sourcePath),this.transformMatrix&&(t.transformMatrix=this.transformMatrix),t},toDatalessObject:function(e){var t=this.toObject(e);return this.sourcePath&&(t.path=this.sourcePath),delete t.sourcePath,t},toSVG:function(e){var t=[],n=this._createBaseSVGMarkup();for(var r=0,i=this.path.length;r',"",""),e?e(n.join("")):n.join("")},complexity:function(){return this.path.length},_parsePath:function(){var e=[],n=[],r,i,s=/(-?\.\d+)|(-?\d+(\.\d+)?)/g,o,u;for(var a=0,f,l=this.path.length;ad)for(var v=1,m=f.length;v"];for(var r=0,i=t.length;r"),e?e(n.join("")):n.join("")},toString:function(){return"#"},isSameColor:function(){var e=this.getObjects()[0].get("fill");return this.getObjects().every(function(t){return t.get("fill")===e})},complexity:function(){return this.paths.reduce(function(e,t){return e+(t&&t.complexity?t.complexity():0)},0)},getObjects:function(){return this.paths}}),t.PathGroup.fromObject=function(e,n){typeof e.paths=="string"?t.loadSVGFromURL(e.paths,function(r){var i=e.paths;delete e.paths;var s=t.util.groupSVGElements(r,e,i);n(s)}):t.util.enlivenObjects(e.paths,function(r){delete e.paths,n(new t.PathGroup(r,e))})},t.PathGroup.async=!0}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.array.min,i=t.util.array.max,s=t.util.array.invoke;if(t.Group)return;var o={lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0};t.Group=t.util.createClass(t.Object,t.Collection,{type:"group",initialize:function(e,t){t=t||{},this._objects=e||[];for(var r=this._objects.length;r--;)this._objects[r].group=this;this.originalState={},this.callSuper("initialize"),this._calcBounds(),this._updateObjectsCoords(),t&&n(this,t),this._setOpacityIfSame(),this.setCoords(!0),this.saveCoords()},_updateObjectsCoords:function(){var e=this.left,t=this.top;this.forEachObject(function(n){var r=n.get("left"),i=n.get("top");n.set("originalLeft",r),n.set("originalTop",i),n.set("left",r-e),n.set("top",i-t),n.setCoords(),n.__origHasControls=n.hasControls,n.hasControls=!1},this)},toString:function(){return"#"},getObjects:function(){return this._objects},addWithUpdate:function(e){return this._restoreObjectsState(),this._objects.push(e),e.group=this,this.forEachObject(function(e){e.set("active",!0),e.group=this},this),this._calcBounds(),this._updateObjectsCoords(),this},removeWithUpdate:function(e){return this._moveFlippedObject(e),this._restoreObjectsState(),this.forEachObject(function(e){e.set("active",!0),e.group=this},this),this.remove(e),this._calcBounds(),this._updateObjectsCoords(),this},_onObjectAdded:function(e){e.group=this},_onObjectRemoved:function(e){delete e.group,e.set("active",!1)},delegatedProperties:{fill:!0,opacity:!0,fontFamily:!0,fontWeight:!0,fontSize:!0,fontStyle:!0,lineHeight:!0,textDecoration:!0,textAlign:!0,backgroundColor:!0},_set:function(e,t){if(e in this.delegatedProperties){var n=this._objects.length;this[e]=t;while(n--)this._objects[n].set(e,t)}else this[e]=t},toObject:function(e){return n(this.callSuper("toObject",e),{objects:s(this._objects,"toObject",e)})},render:function(e,n){if(!this.visible)return;e.save(),this.transform(e);var r=Math.max(this.scaleX,this.scaleY);this.clipTo&&t.util.clipContext(this,e);for(var i=0,s=this._objects.length;i'];for(var n=0,r=this._objects.length;n"),e?e(t.join("")):t.join("")},get:function(e){if(e in o){if(this[e])return this[e];for(var t=0,n=this._objects.length;t','');if(this.stroke||this.strokeDashArray){var n=this.fill;this.fill=null,t.push("'),this.fill=n}return t.push(""),e?e(t.join("")):t.join("")},getSrc:function(){return this.getElement().src||this.getElement()._src},toString:function(){return'#'},clone:function(e,t){this.constructor.fromObject(this.toObject(t),e)},applyFilters:function(e){if(this.filters.length===0){this._element=this._originalElement,e&&e();return}var t=this._originalElement,n=fabric.util.createCanvasElement(),r=fabric.util.createImage(),i=this;return n.width=t.width,n.height=t.height,n.getContext("2d").drawImage(t,0,0,t.width,t.height),this.filters.forEach(function(e){e&&e.applyTo(n)}),r.width=t.width,r.height=t.height,fabric.isLikelyNode?(r.src=n.toBuffer(undefined,fabric.Image.pngCompression),i._element=r,e&&e()):(r.onload=function(){i._element=r,e&&e(),r.onload=n=t=null},r.src=n.toDataURL("image/png")),this},_render:function(e){e.drawImage(this._element,-this.width/2,-this.height/2,this.width,this.height)},_resetWidthHeight:function(){var e=this.getElement();this.set("width",e.width),this.set("height",e.height)},_initElement:function(e){this.setElement(fabric.util.getById(e)),fabric.util.addClass(this.getElement(),fabric.Image.CSS_CANVAS)},_initConfig:function(e){e||(e={}),this.setOptions(e),this._setWidthHeight(e)},_initFilters:function(e,t){e.filters&&e.filters.length?fabric.util.enlivenObjects(e.filters,function(e){t&&t(e)},"fabric.Image.filters"):t&&t()},_setWidthHeight:function(e){this.width="width"in e?e.width:this.getElement().width||0,this.height="height"in e?e.height:this.getElement().height||0},complexity:function(){return 1}}),fabric.Image.CSS_CANVAS="canvas-img",fabric.Image.prototype.getSvgSrc=fabric.Image.prototype.getSrc,fabric.Image.fromObject=function(e,t){var n=fabric.document.createElement("img"),r=e.src;n.onload=function(){fabric.Image.prototype._initFilters.call(e,e,function(r){e.filters=r||[];var i=new fabric.Image(n,e);t&&t(i),n=n.onload=n.onerror=null})},n.onerror=function(){fabric.log("Error loading "+n.src),t&&t(null,!0),n=n.onload=n.onerror=null},n.src=r},fabric.Image.fromURL=function(e,t,n){fabric.util.loadImage(e,function(e){t(new fabric.Image(e,n))})},fabric.Image.ATTRIBUTE_NAMES=fabric.SHARED_ATTRIBUTES.concat("x y width height xlink:href".split(" ")),fabric.Image.fromElement=function(e,n,r){var i=fabric.parseAttributes(e,fabric.Image.ATTRIBUTE_NAMES);fabric.Image.fromURL(i["xlink:href"],n,t(r?fabric.util.object.clone(r):{},i))},fabric.Image.async=!0,fabric.Image.pngCompression=1}(typeof exports!="undefined"?exports:this),fabric.util.object.extend(fabric.Object.prototype,{_getAngleValueForStraighten:function(){var e=this.getAngle()%360;return e>0?Math.round((e-1)/90)*90:Math.round(e/90)*90},straighten:function(){return this.setAngle(this._getAngleValueForStraighten()),this},fxStraighten:function( -e){e=e||{};var t=function(){},n=e.onComplete||t,r=e.onChange||t,i=this;return fabric.util.animate({startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(e){i.setAngle(e),r()},onComplete:function(){i.setCoords(),n()},onStart:function(){i.set("active",!1)}}),this}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{straightenObject:function(e){return e.straighten(),this.renderAll(),this},fxStraightenObject:function(e){return e.fxStraighten({onChange:this.renderAll.bind(this)}),this}}),fabric.Image.filters=fabric.Image.filters||{},fabric.Image.filters.BaseFilter=fabric.util.createClass({type:"BaseFilter",toObject:function(){return{type:this.type}},toJSON:function(){return this.toObject()}}),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Brightness=t.util.createClass(t.Image.filters.BaseFilter,{type:"Brightness",initialize:function(e){e=e||{},this.brightness=e.brightness||100},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.brightness;for(var s=0,o=r.length;s=0&&N=0&&C-1?e.channel:0},applyTo:function(e){if(!this.mask)return;var n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=r.data,s=this.mask.getElement(),o=t.util.createCanvasElement(),u=this.channel,a,f=r.width*r.height*4;o.width=s.width,o.height=s.height,o.getContext("2d").drawImage(s,0,0,s.width,s.height);var l=o.getContext("2d").getImageData(0,0,s.width,s.height),c=l.data;for(a=0;ao&&f>o&&l>o&&u(a-f)'},_render:function(e){var t=this.group&&this.group.type!=="group";t&&!this.transformMatrix?e.translate(-this.group.width/2+this.left,-this.group.height/2+this.top):t&&this.transformMatrix&&e.translate(-this.group.width/2,-this.group.height/2),typeof Cufon=="undefined"||this.useNative===!0?this._renderViaNative(e):this._renderViaCufon(e)},_renderViaNative:function(e){this.transform(e,t.isLikelyNode),this._setTextStyles(e);var n=this.text.split(/\r?\n/);this.width=this._getTextWidth(e,n),this.height=this._getTextHeight(e,n),this.clipTo&&t.util.clipContext(this,e),this._renderTextBackground(e,n),this.textAlign!=="left"&&this.textAlign!=="justify"&&(e.save(),e.translate(this.textAlign==="center"?this.width/2:this.width,0)),e.save(),this._setShadow(e),this._renderTextFill(e,n),this._renderTextStroke(e,n),this._removeShadow(e),e.restore(),this.textAlign!=="left"&&this.textAlign!=="justify"&&e.restore(),this._renderTextDecoration(e,n),this.clipTo&&e.restore(),this._setBoundaries(e,n),this._totalLineHeight=0},_setBoundaries:function(e,t){this._boundaries=[];for(var n=0,r=t.length;nn&&(n=s)}return n},_drawChars:function(e,t,n,r,i){t[e](n,r,i)},_drawTextLine:function(e,t,n,r,i,s){i-=this.fontSize/4;if(this.textAlign!=="justify"){this._drawChars(e,t,n,r,i,s);return}var o=t.measureText(n).width,u=this.width;if(u>o){var a=n.split(/\s+/),f=t.measureText(n.replace(/\s+/g,"")).width,l=u-f,c=a.length-1,h=l/c,p=0;for(var d=0,v=a.length;d-1&&i(this.fontSize*this.lineHeight),this.textDecoration.indexOf("line-through")>-1&&i(this.fontSize*this.lineHeight-this.fontSize/2),this.textDecoration.indexOf("overline")>-1&&i(this.fontSize*this.lineHeight-this.fontSize)},_getFontDeclaration:function(){return[t.isLikelyNode?this.fontWeight:this.fontStyle,t.isLikelyNode?this.fontStyle:this.fontWeight,this.fontSize+"px",t.isLikelyNode?'"'+this.fontFamily+'"':this.fontFamily].join(" ")},render:function(e,t){if(!this.visible)return;e.save(),this._render(e),!t&&this.active&&(this.drawBorders(e),this.drawControls(e)),e.restore()},toObject:function(e){var t=n(this.callSuper("toObject",e),{text:this.text,fontSize:this.fontSize,fontWeight:this.fontWeight,fontFamily:this.fontFamily,fontStyle:this.fontStyle,lineHeight:this.lineHeight,textDecoration:this.textDecoration,textAlign:this.textAlign,path:this.path,textBackgroundColor:this.textBackgroundColor,useNative:this.useNative});return this.includeDefaultValues||this._removeDefaultValues(t),t},toSVG:function(e){var t=[],n=this.text.split(/\r?\n/),r=this.useNative?this.fontSize*this.lineHeight:-this._fontAscent-this._fontAscent/5*this.lineHeight,s=-(this.width/2),o=this.useNative?this.fontSize-1:this.height/2-n.length*this.fontSize-this._totalLineHeight,u=this._getSVGTextAndBg(r,s,n),a=this._getSVGShadows(r,n);return o+=this._fontAscent?this._fontAscent/5*this.lineHeight:0,t.push('',u.textBgRects.join(""),"',a.join(""),u.textSpans.join(""),"",""),e?e(t.join("")):t.join("")},_getSVGShadows:function(e,n){var r=[],s,o,u=1;if(!this.shadow||!this._boundaries)return r;for(s=0,o=n.length;s",t.util.string.escapeXml(n[s]),""),u=1}else u++;return r},_getSVGTextAndBg:function(e,n,r){var s=[],o=[],u,a,f,l=1;this.backgroundColor&&this._boundaries&&o.push("');for(u=0,f=r.length;u",t.util.string.escapeXml(r[u]),""),l=1):l++;if(!this.textBackgroundColor||!this._boundaries)continue;o.push("')}return{textSpans:s,textBgRects:o}},_getFillAttributes:function(e){var n=e&&typeof e=="string"?new t.Color(e):"";return!n||!n.getSource()||n.getAlpha()===1?'fill="'+e+'"':'opacity="'+n.getAlpha()+'" fill="'+n.setAlpha(1).toRgb()+'"'},_set:function(e,t){e==="fontFamily"&&this.path&&(this.path=this.path.replace(/(.*?)([^\/]*)(\.font\.js)/,"$1"+t+"$3")),this.callSuper("_set",e,t),e in this._dimensionAffectingProps&&(this._initDimensions(),this.setCoords())},complexity:function(){return 1}}),t.Text.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x y font-family font-style font-weight font-size text-decoration".split(" ")),t.Text.fromElement=function(e,n){if(!e)return null;var r=t.parseAttributes(e,t.Text.ATTRIBUTE_NAMES);n=t.util.object.extend(n?t.util.object.clone(n):{},r);var i=new t.Text(e.textContent,n);return i.set({left:i.getLeft()+i.getWidth()/2,top:i.getTop()-i.getHeight()/2}),i},t.Text.fromObject=function(e){return new t.Text(e.text,r(e))},t.util.createAccessors(t.Text)}(typeof exports!="undefined"?exports:this),fabric.util.object.extend(fabric.Text.prototype,{_renderViaCufon:function(e){var t=Cufon.textOptions||(Cufon.textOptions={});t.left=this.left,t.top=this.top,t.context=e,t.color=this.fill;var n=this._initDummyElementForCufon();this.transform(e),Cufon.replaceElement(n,{engine:"canvas",separate:"none",fontFamily:this.fontFamily,fontWeight:this.fontWeight,textDecoration:this.textDecoration,textShadow:this.shadow&&this.shadow.toString(),textAlign:this.textAlign,fontStyle:this.fontStyle,lineHeight:this.lineHeight,stroke:this.stroke,strokeWidth:this.strokeWidth,backgroundColor:this.backgroundColor,textBackgroundColor:this.textBackgroundColor}),this.width=t.width,this.height=t.height,this._totalLineHeight=t.totalLineHeight,this._fontAscent=t.fontAscent,this._boundaries=t.boundaries,n=null,this.setCoords()},_initDummyElementForCufon:function(){var e=fabric.document.createElement("pre"),t=fabric.document.createElement("div");return t.appendChild(e),typeof G_vmlCanvasManager=="undefined"?e.innerHTML=this.text:e.innerText=this.text.replace(/\r?\n/gi,"\r"),e.style.fontSize=this.fontSize+"px",e.style.letterSpacing="normal",e}}),function(){function request(e,t,n){var r=URL.parse(e);r.port||(r.port=r.protocol.indexOf("https:")===0?443:80);var i=r.port===443?HTTPS:HTTP,s=i.request({hostname:r.hostname,port:r.port,path:r.path,method:"GET"},function(e){var r="";t&&e.setEncoding(t),e.on("end",function(){n(r)}),e.on("data",function(t){e.statusCode===200&&(r+=t)})});s.on("error",function(e){e.errno===process.ECONNREFUSED?fabric.log("ECONNREFUSED: connection refused to "+r.hostname+":"+r.port):fabric.log(e.message)}),s.end()}function request_fs(e,t){var n=require("fs");n.readFile(e,function(e,n){if(e)throw fabric.log(e),e;t(n)})}if(typeof document!="undefined"&&typeof window!="undefined")return;var DOMParser=(new require("xmldom")).DOMParser,URL=require("url"),HTTP=require("http"),HTTPS=require("https"),Canvas=require("canvas"),Image=require("canvas").Image;fabric.util.loadImage=function(e,t,n){var r=function(r){i.src=new Buffer(r,"binary"),i._src=e,t&&t.call(n,i)},i=new Image;e&&(e instanceof Buffer||e.indexOf("data")===0)?(i.src=i._src=e,t&&t.call(n,i)):e&&e.indexOf("http")!==0?request_fs(e,r):e&&request(e,"binary",r)},fabric.loadSVGFromURL=function(e,t,n){e=e.replace(/^\n\s*/,"").replace(/\?.*$/,"").trim(),e.indexOf("http")!==0?request_fs(e,function(e){fabric.loadSVGFromString(e,t,n)}):request(e,"",function(e){fabric.loadSVGFromString(e,t,n)})},fabric.loadSVGFromString=function(e,t,n){var r=(new DOMParser).parseFromString(e);fabric.parseSVGDocument(r.documentElement,function(e,n){t&&t(e,n)},n)},fabric.util.getScript=function(url,callback){request(url,"",function(body){eval(body),callback&&callback()})},fabric.Image.fromObject=function(e,t){fabric.util.loadImage(e.src,function(n){var r=new fabric.Image(n);r._initConfig(e),r._initFilters(e,function(e){r.filters=e||[],t&&t(r)})})},fabric.createCanvasForNode=function(e,t){var n=fabric.document.createElement("canvas"),r=new Canvas(e||600,t||600);n.style={},n.width=r.width,n.height=r.height;var i=fabric.Canvas||fabric.StaticCanvas,s=new i(n);return s.contextContainer=r.getContext("2d"),s.nodeCanvas=r,s.Font=Canvas.Font,s},fabric.StaticCanvas.prototype.createPNGStream=function(){return this.nodeCanvas.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(e){return this.nodeCanvas.createJPEGStream(e)};var origSetWidth=fabric.StaticCanvas.prototype.setWidth;fabric.StaticCanvas.prototype.setWidth=function(e){return origSetWidth.call(this,e),this.nodeCanvas.width=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setWidth=fabric.StaticCanvas.prototype.setWidth);var origSetHeight=fabric.StaticCanvas.prototype.setHeight;fabric.StaticCanvas.prototype.setHeight=function(e){return origSetHeight.call(this,e),this.nodeCanvas.height=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setHeight=fabric.StaticCanvas.prototype.setHeight)}(); \ No newline at end of file +/* build: `node build.js modules=ALL exclude=gestures,cufon,json minifier=uglifyjs` *//*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */var fabric=fabric||{version:"1.3.12"};typeof exports!="undefined"&&(exports.fabric=fabric),typeof document!="undefined"&&typeof window!="undefined"?(fabric.document=document,fabric.window=window):(fabric.document=require("jsdom").jsdom(""),fabric.window=fabric.document.createWindow()),fabric.isTouchSupported="ontouchstart"in fabric.document.documentElement,fabric.isLikelyNode=typeof Buffer!="undefined"&&typeof window=="undefined",fabric.SHARED_ATTRIBUTES=["transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width"],function(){function e(e,t){if(!this.__eventListeners[e])return;t?fabric.util.removeFromArray(this.__eventListeners[e],t):this.__eventListeners[e].length=0}function t(e,t){this.__eventListeners||(this.__eventListeners={});if(arguments.length===1)for(var n in e)this.on(n,e[n]);else this.__eventListeners[e]||(this.__eventListeners[e]=[]),this.__eventListeners[e].push(t);return this}function n(t,n){if(!this.__eventListeners)return;if(arguments.length===0)this.__eventListeners={};else if(arguments.length===1&&typeof arguments[0]=="object")for(var r in t)e.call(this,r,t[r]);else e.call(this,t,n);return this}function r(e,t){if(!this.__eventListeners)return;var n=this.__eventListeners[e];if(!n)return;for(var r=0,i=n.length;r-1},complexity:function(){return this.getObjects().reduce(function(e,t){return e+=t.complexity?t.complexity():0,e},0)}},function(e){var t=Math.sqrt,n=Math.atan2,r=Math.PI/180;fabric.util={removeFromArray:function(e,t){var n=e.indexOf(t);return n!==-1&&e.splice(n,1),e},getRandomInt:function(e,t){return Math.floor(Math.random()*(t-e+1))+e},degreesToRadians:function(e){return e*r},radiansToDegrees:function(e){return e/r},rotatePoint:function(e,t,n){var r=Math.sin(n),i=Math.cos(n);e.subtractEquals(t);var s=e.x*i-e.y*r,o=e.x*r+e.y*i;return(new fabric.Point(s,o)).addEquals(t)},toFixed:function(e,t){return parseFloat(Number(e).toFixed(t))},falseFunction:function(){return!1},getKlass:function(e,t){return e=fabric.util.string.camelize(e.charAt(0).toUpperCase()+e.slice(1)),fabric.util.resolveNamespace(t)[e]},resolveNamespace:function(t){if(!t)return fabric;var n=t.split("."),r=n.length,i=e||fabric.window;for(var s=0;s1?r=new fabric.PathGroup(e,t):r=e[0],typeof n!="undefined"&&r.setSourcePath(n),r},populateWithProperties:function(e,t,n){if(n&&Object.prototype.toString.call(n)==="[object Array]")for(var r=0,i=n.length;rr)r+=u[p++%h],r>l&&(r=l),e[d?"lineTo":"moveTo"](r,0),d=!d;e.restore()},createCanvasElement:function(e){return e||(e=fabric.document.createElement("canvas")),!e.getContext&&typeof G_vmlCanvasManager!="undefined"&&G_vmlCanvasManager.initElement(e),e},createImage:function(){return fabric.isLikelyNode?new(require("canvas").Image):fabric.document.createElement("img")},createAccessors:function(e){var t=e.prototype;for(var n=t.stateProperties.length;n--;){var r=t.stateProperties[n],i=r.charAt(0).toUpperCase()+r.slice(1),s="set"+i,o="get"+i;t[o]||(t[o]=function(e){return new Function('return this.get("'+e+'")')}(r)),t[s]||(t[s]=function(e){return new Function("value",'return this.set("'+e+'", value)')}(r))}},clipContext:function(e,t){t.save(),t.beginPath(),e.clipTo(t),t.clip()},multiplyTransformMatrices:function(e,t){var n=[[e[0],e[2],e[4]],[e[1],e[3],e[5]],[0,0,1]],r=[[t[0],t[2],t[4]],[t[1],t[3],t[5]],[0,0,1]],i=[];for(var s=0;s<3;s++){i[s]=[];for(var o=0;o<3;o++){var u=0;for(var a=0;a<3;a++)u+=n[s][a]*r[a][o];i[s][o]=u}}return[i[0][0],i[1][0],i[0][1],i[1][1],i[0][2],i[1][2]]},getFunctionBody:function(e){return(String(e).match(/function[^{]*\{([\s\S]*)\}/)||{})[1]},normalizePoints:function(e,t){var n=fabric.util.array.min(e,"x"),r=fabric.util.array.min(e,"y");n=n<0?n:0,r=n<0?r:0;for(var i=0,s=e.length;i0&&(t>r?t-=r:t=0,n>r?n-=r:n=0);var i=!0,s=e.getImageData(t,n,r*2||1,r*2||1);for(var o=3,u=s.data.length;o0&&f===0&&(E-=2*Math.PI);var S=Math.ceil(Math.abs(E/(Math.PI*.5+.001))),x=[];for(var T=0;T1&&(h=Math.sqrt(h),t*=h,n*=h);var p=f/t,d=a/t,v=-a/n,m=f/n;return{x0:p*r+d*i,y0:v*r+m*i,x1:p*s+d*o,y1:v*s+m*o,sin_th:a,cos_th:f}}function o(e,i,s,o,u,a,f,l){r=n.call(arguments);if(t[r])return t[r];var c=l*u,h=-f*a,p=f*u,d=l*a,v=.5*(o-s),m=8/3*Math.sin(v*.5)*Math.sin(v*.5)/Math.sin(v),g=e+Math.cos(s)-m*Math.sin(s),y=i+Math.sin(s)+m*Math.cos(s),b=e+Math.cos(o),w=i+Math.sin(o),E=b+m*Math.sin(o),S=w-m*Math.cos(o);return t[r]=[c*g+h*y,p*g+d*y,c*E+h*S,p*E+d*S,c*b+h*w,p*b+d*w],t[r]}var e={},t={},n=Array.prototype.join,r;fabric.util.drawArc=function(e,t,n,r){var s=r[0],u=r[1],a=r[2],f=r[3],l=r[4],c=r[5],h=r[6],p=i(c,h,s,u,f,l,a,t,n);for(var d=0;d=t})}function r(e,t){return i(e,t,function(e,t){return e>>0;if(n===0)return-1;var r=0;arguments.length>0&&(r=Number(arguments[1]),r!==r?r=0:r!==0&&r!==Number.POSITIVE_INFINITY&&r!==Number.NEGATIVE_INFINITY&&(r=(r>0||-1)*Math.floor(Math.abs(r))));if(r>=n)return-1;var i=r>=0?r:Math.max(n-Math.abs(r),0);for(;i>>0;n>>0;r>>0;n>>0;n>>0;i>>0,n=0,r;if(arguments.length>1)r=arguments[1];else do{if(n in this){r=this[n++];break}if(++n>=t)throw new TypeError}while(!0);for(;n/g,">")}String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\xA0]+/,"").replace(/[\s\xA0]+$/,"")}),fabric.util.string={camelize:e,capitalize:t,escapeXml:n}}(),function(){var e=Array.prototype.slice,t=Function.prototype.apply,n=function(){};Function.prototype.bind||(Function.prototype.bind=function(r){var i=this,s=e.call(arguments,1),o;return s.length?o=function(){return t.call(i,this instanceof n?this:r,s.concat(e.call(arguments)))}:o=function(){return t.call(i,this instanceof n?this:r,arguments)},n.prototype=this.prototype,o.prototype=new n,o})}(),function(){function i(){}function s(t){var n=this.constructor.superclass.prototype[t];return arguments.length>1?n.apply(this,e.call(arguments,1)):n.call(this)}function o(){function u(){this.initialize.apply(this,arguments)}var n=null,o=e.call(arguments,0);typeof o[0]=="function"&&(n=o.shift()),u.superclass=n,u.subclasses=[],n&&(i.prototype=n.prototype,u.prototype=new i,n.subclasses.push(u));for(var a=0,f=o.length;a-1?e.prototype[i]=function(e){return function(){var n=this.constructor.superclass;this.constructor.superclass=r;var i=t[e].apply(this,arguments);this.constructor.superclass=n;if(e!=="initialize")return i}}(i):e.prototype[i]=t[i],n&&(t.toString!==Object.prototype.toString&&(e.prototype.toString=t.toString),t.valueOf!==Object.prototype.valueOf&&(e.prototype.valueOf=t.valueOf))};fabric.util.createClass=o}(),function(){function t(e){var t=Array.prototype.slice.call(arguments,1),n,r,i=t.length;for(r=0;r-1?s(e,t.match(/opacity:\s*(\d?\.?\d*)/)[1]):e;for(var r in t)if(r==="opacity")s(e,t[r]);else{var i=r==="float"||r==="cssFloat"?typeof n.styleFloat=="undefined"?"cssFloat":"styleFloat":r;n[i]=t[r]}return e}var t=fabric.document.createElement("div"),n=typeof t.style.opacity=="string",r=typeof t.style.filter=="string",i=/alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,s=function(e){return e};n?s=function(e,t){return e.style.opacity=t,e}:r&&(s=function(e,t){var n=e.style;return e.currentStyle&&!e.currentStyle.hasLayout&&(n.zoom=1),i.test(n.filter)?(t=t>=.9999?"":"alpha(opacity="+t*100+")",n.filter=n.filter.replace(i,t)):n.filter+=" alpha(opacity="+t*100+")",e}),fabric.util.setStyle=e}(),function(){function t(e){return typeof e=="string"?fabric.document.getElementById(e):e}function s(e,t){var n=fabric.document.createElement(e);for(var r in t)r==="class"?n.className=t[r]:r==="for"?n.htmlFor=t[r]:n.setAttribute(r,t[r]);return n}function o(e,t){(" "+e.className+" ").indexOf(" "+t+" ")===-1&&(e.className+=(e.className?" ":"")+t)}function u(e,t,n){return typeof t=="string"&&(t=s(t,n)),e.parentNode&&e.parentNode.replaceChild(t,e),t.appendChild(e),t}function a(e,t){var n,r,i=0,s=0,o=fabric.document.documentElement,u=fabric.document.body||{scrollLeft:0,scrollTop:0};r=e;while(e&&e.parentNode&&!n)e=e.parentNode,e!==fabric.document&&fabric.util.getElementStyle(e,"position")==="fixed"&&(n=e),e!==fabric.document&&r!==t&&fabric.util.getElementStyle(e,"position")==="absolute"?(i=0,s=0):e===fabric.document?(i=u.scrollLeft||o.scrollLeft||0,s=u.scrollTop||o.scrollTop||0):(i+=e.scrollLeft||0,s+=e.scrollTop||0);return{left:i,top:s}}function f(e){var t,n={left:0,top:0},r=e&&e.ownerDocument,i={left:0,top:0},s,o={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!r)return{left:0,top:0};for(var u in o)i[o[u]]+=parseInt(l(e,u),10)||0;return t=r.documentElement,typeof e.getBoundingClientRect!="undefined"&&(n=e.getBoundingClientRect()),s=fabric.util.getScrollLeftTop(e,null),{left:n.left+s.left-(t.clientLeft||0)+i.left,top:n.top+s.top-(t.clientTop||0)+i.top}}function l(e,t){e.style||(e.style={});if(fabric.document.defaultView&&fabric.document.defaultView.getComputedStyle)return fabric.document.defaultView.getComputedStyle(e,null)[t];var n=e.style[t];return!n&&e.currentStyle&&(n=e.currentStyle[t]),n}var e=Array.prototype.slice,n=function(t){return e.call(t,0)},r;try{r=n(fabric.document.childNodes)instanceof Array}catch(i){}r||(n=function(e){var t=new Array(e.length),n=e.length;while(n--)t[n]=e[n];return t}),function(){function n(e){return typeof e.onselectstart!="undefined"&&(e.onselectstart=fabric.util.falseFunction),t?e.style[t]="none":typeof e.unselectable=="string"&&(e.unselectable="on"),e}function r(e){return typeof e.onselectstart!="undefined"&&(e.onselectstart=null),t?e.style[t]="":typeof e.unselectable=="string"&&(e.unselectable=""),e}var e=fabric.document.documentElement.style,t="userSelect"in e?"userSelect":"MozUserSelect"in e?"MozUserSelect":"WebkitUserSelect"in e?"WebkitUserSelect":"KhtmlUserSelect"in e?"KhtmlUserSelect":"";fabric.util.makeElementUnselectable=n,fabric.util.makeElementSelectable=r}(),function(){function e(e,t){var n=fabric.document.getElementsByTagName("head")[0],r=fabric.document.createElement("script"),i=!0;r.onload=r.onreadystatechange=function(e){if(i){if(typeof this.readyState=="string"&&this.readyState!=="loaded"&&this.readyState!=="complete")return;i=!1,t(e||fabric.window.event),r=r.onload=r.onreadystatechange=null}},r.src=e,n.appendChild(r)}fabric.util.getScript=e}(),fabric.util.getById=t,fabric.util.toArray=n,fabric.util.makeElement=s,fabric.util.addClass=o,fabric.util.wrapElement=u,fabric.util.getScrollLeftTop=a,fabric.util.getElementOffset=f,fabric.util.getElementStyle=l}(),function(){function e(e,t){return e+(/\?/.test(e)?"&":"?")+t}function n(){}function r(r,i){i||(i={});var s=i.method?i.method.toUpperCase():"GET",o=i.onComplete||function(){},u=t(),a;return u.onreadystatechange=function(){u.readyState===4&&(o(u),u.onreadystatechange=n)},s==="GET"&&(a=null,typeof i.parameters=="string"&&(r=e(r,i.parameters))),u.open(s,r,!0),(s==="POST"||s==="PUT")&&u.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),u.send(a),u}var t=function(){var e=[function(){return new ActiveXObject("Microsoft.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP.3.0")},function(){return new XMLHttpRequest}];for(var t=e.length;t--;)try{var n=e[t]();if(n)return e[t]}catch(r){}}();fabric.util.request=r}(),fabric.log=function(){},fabric.warn=function(){},typeof console!="undefined"&&["log","warn"].forEach(function(e){typeof console[e]!="undefined"&&console[e].apply&&(fabric[e]=function(){return console[e].apply(console,arguments)})}),function(){function e(e){n(function(t){e||(e={});var r=t||+(new Date),i=e.duration||500,s=r+i,o,u=e.onChange||function(){},a=e.abort||function(){return!1},f=e.easing||function(e,t,n,r){return-n*Math.cos(e/r*(Math.PI/2))+n+t},l="startValue"in e?e.startValue:0,c="endValue"in e?e.endValue:100,h=e.byValue||c-l;e.onStart&&e.onStart(),function p(t){o=t||+(new Date);var c=o>s?i:o-r;if(a()){e.onComplete&&e.onComplete();return}u(f(c,l,h,i));if(o>s){e.onComplete&&e.onComplete();return}n(p)}(r)})}var t=fabric.window.requestAnimationFrame||fabric.window.webkitRequestAnimationFrame||fabric.window.mozRequestAnimationFrame||fabric.window.oRequestAnimationFrame||fabric.window.msRequestAnimationFrame||function(e){fabric.window.setTimeout(e,1e3/60)},n=function(){return t.apply(fabric.window,arguments)};fabric.util.animate=e,fabric.util.requestAnimFrame=n}(),function(){function e(e,t,n,r){return e','')}var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.string.capitalize,i=t.util.object.clone,s=t.util.toFixed,o=t.util.multiplyTransformMatrices,u={"fill-opacity":"fillOpacity","fill-rule":"fillRule","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight",cx:"left",x:"left",r:"radius","stroke-dasharray":"strokeDashArray","stroke-linecap":"strokeLineCap","stroke-linejoin":"strokeLineJoin","stroke-miterlimit":"strokeMiterLimit","stroke-opacity":"strokeOpacity","stroke-width":"strokeWidth","text-decoration":"textDecoration",cy:"top",y:"top",transform:"transformMatrix"},a={stroke:"strokeOpacity",fill:"fillOpacity"};t.parseTransformAttribute=function(){function e(e,t){var n=t[0];e[0]=Math.cos(n),e[1]=Math.sin(n),e[2]=-Math.sin(n),e[3]=Math.cos(n)}function n(e,t){var n=t[0],r=t.length===2?t[1]:t[0];e[0]=n,e[3]=r}function r(e,t){e[2]=t[0]}function i(e,t){e[1]=t[0]}function s(e,t){e[4]=t[0],t.length===2&&(e[5]=t[1])}var o=[1,0,0,1,0,0],u="(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)",a="(?:\\s+,?\\s*|,\\s*)",f="(?:(skewX)\\s*\\(\\s*("+u+")\\s*\\))",l="(?:(skewY)\\s*\\(\\s*("+u+")\\s*\\))",c="(?:(rotate)\\s*\\(\\s*("+u+")(?:"+a+"("+u+")"+a+"("+u+"))?\\s*\\))",h="(?:(scale)\\s*\\(\\s*("+u+")(?:"+a+"("+u+"))?\\s*\\))",p="(?:(translate)\\s*\\(\\s*("+u+")(?:"+a+"("+u+"))?\\s*\\))",d="(?:(matrix)\\s*\\(\\s*("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+"\\s*\\))",v="(?:"+d+"|"+p+"|"+h+"|"+c+"|"+f+"|"+l+")",m="(?:"+v+"(?:"+a+v+")*"+")",g="^\\s*(?:"+m+"?)\\s*$",y=new RegExp(g),b=new RegExp(v,"g");return function(u){var a=o.concat(),f=[];if(!u||u&&!y.test(u))return a;u.replace(b,function(t){var u=(new RegExp(v)).exec(t).filter(function(e){return e!==""&&e!=null}),l=u[1],c=u.slice(2).map(parseFloat);switch(l){case"translate":s(a,c);break;case"rotate":e(a,c);break;case"scale":n(a,c);break;case"skewX":r(a,c);break;case"skewY":i(a,c);break;case"matrix":a=c}f.push(a.concat()),a=o.concat()});var l=f[0];while(f.length>1)f.shift(),l=t.util.multiplyTransformMatrices(l,f[0]);return l}}(),t.parseSVGDocument=function(){function s(e,t){while(e&&(e=e.parentNode))if(t.test(e.nodeName))return!0;return!1}var e=/^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/,n="(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)",r=new RegExp("^\\s*("+n+"+)\\s*,?"+"\\s*("+n+"+)\\s*,?"+"\\s*("+n+"+)\\s*,?"+"\\s*("+n+"+)\\s*"+"$");return function(n,o,u){if(!n)return;var a=new Date,f=t.util.toArray(n.getElementsByTagName("*"));if(f.length===0){f=n.selectNodes("//*[name(.)!='svg']");var l=[];for(var c=0,h=f.length;c-1;e=e.split(/\s+/);var n=[],r,i;if(t){r=0,i=e.length;for(;r/i,"")));if(!s.documentElement)return;t.parseSVGDocument(s.documentElement,function(r,i){m.set(e,{objects:t.util.array.invoke(r,"toObject"),options:i}),n(r,i)},r)}e=e.replace(/^\n\s*/,"").trim(),m.has(e,function(r){r?m.get(e,function(e){var t=g(e);n(t.objects,t.options)}):new t.util.request(e,{method:"get",onComplete:i})})},loadSVGFromString:function(e,n,r){e=e.trim();var i;if(typeof DOMParser!="undefined"){var s=new DOMParser;s&&s.parseFromString&&(i=s.parseFromString(e,"text/xml"))}else t.window.ActiveXObject&&(i=new ActiveXObject("Microsoft.XMLDOM"),i.async="false",i.loadXML(e.replace(//i,"")));t.parseSVGDocument(i.documentElement,function(e,t){n(e,t)},r)},createSVGFontFacesMarkup:function(e){var t="";for(var n=0,r=e.length;n',"",""].join("")),t},createSVGRefElementsMarkup:function(e){var t=[];return y(t,e,"backgroundColor"),y(t,e,"overlayColor"),t.join("")}})}(typeof exports!="undefined"?exports:this),fabric.ElementsParser={parse:function(e,t,n,r){this.elements=e,this.callback=t,this.options=n,this.reviver=r,this.instances=new Array(e.length),this.numElements=e.length,this.createObjects()},createObjects:function(){for(var e=0,t=this.elements.length;ee.x&&this.y>e.y},gte:function(e){return this.x>=e.x&&this.y>=e.y},lerp:function(e,t){return new n(this.x+(e.x-this.x)*t,this.y+(e.y-this.y)*t)},distanceFrom:function(e){var t=this.x-e.x,n=this.y-e.y;return Math.sqrt(t*t+n*n)},midPointFrom:function(e){return new n(this.x+(e.x-this.x)/2,this.y+(e.y-this.y)/2)},min:function(e){return new n(Math.min(this.x,e.x),Math.min(this.y,e.y))},max:function(e){return new n(Math.max(this.x,e.x),Math.max(this.y,e.y))},toString:function(){return this.x+","+this.y},setXY:function(e,t){this.x=e,this.y=t},setFromPoint:function(e){this.x=e.x,this.y=e.y},swap:function(e){var t=this.x,n=this.y;this.x=e.x,this.y=e.y,e.x=t,e.y=n}}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function n(e){this.status=e,this.points=[]}var t=e.fabric||(e.fabric={});if(t.Intersection){t.warn("fabric.Intersection is already defined");return}t.Intersection=n,t.Intersection.prototype={appendPoint:function(e){this.points.push(e)},appendPoints:function(e){this.points=this.points.concat(e)}},t.Intersection.intersectLineLine=function(e,r,i,s){var o,u=(s.x-i.x)*(e.y-i.y)-(s.y-i.y)*(e.x-i.x),a=(r.x-e.x)*(e.y-i.y)-(r.y-e.y)*(e.x-i.x),f=(s.y-i.y)*(r.x-e.x)-(s.x-i.x)*(r.y-e.y);if(f!==0){var l=u/f,c=a/f;0<=l&&l<=1&&0<=c&&c<=1?(o=new n("Intersection"),o.points.push(new t.Point(e.x+l*(r.x-e.x),e.y+l*(r.y-e.y)))):o=new n}else u===0||a===0?o=new n("Coincident"):o=new n("Parallel");return o},t.Intersection.intersectLinePolygon=function(e,t,r){var i=new n,s=r.length;for(var o=0;o0&&(i.status="Intersection"),i},t.Intersection.intersectPolygonPolygon=function(e,t){var r=new n,i=e.length;for(var s=0;s0&&(r.status="Intersection"),r},t.Intersection.intersectPolygonRectangle=function(e,r,i){var s=r.min(i),o=r.max(i),u=new t.Point(o.x,s.y),a=new t.Point(s.x,o.y),f=n.intersectLinePolygon(s,u,e),l=n.intersectLinePolygon(u,o,e),c=n.intersectLinePolygon(o,a,e),h=n.intersectLinePolygon(a,s,e),p=new n;return p.appendPoints(f.points),p.appendPoints(l.points),p.appendPoints(c.points),p.appendPoints(h.points),p.points.length>0&&(p.status="Intersection"),p}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function n(e){e?this._tryParsingColor(e):this.setSource([0,0,0,1])}function r(e,t,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?e+(t-e)*6*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e}var t=e.fabric||(e.fabric={});if(t.Color){t.warn("fabric.Color is already defined.");return}t.Color=n,t.Color.prototype={_tryParsingColor:function(e){var t;e in n.colorNameMap&&(e=n.colorNameMap[e]),t=n.sourceFromHex(e),t||(t=n.sourceFromRgb(e)),t||(t=n.sourceFromHsl(e)),t&&this.setSource(t)},_rgbToHsl:function(e,n,r){e/=255,n/=255,r/=255;var i,s,o,u=t.util.array.max([e,n,r]),a=t.util.array.min([e,n,r]);o=(u+a)/2;if(u===a)i=s=0;else{var f=u-a;s=o>.5?f/(2-u-a):f/(u+a);switch(u){case e:i=(n-r)/f+(n']:this.type==="radial"&&(r=["']);for(var i=0;i');return r.push(this.type==="linear"?"":""),r.join("")},toLive:function(e){var t;if(!this.type)return;this.type==="linear"?t=e.createLinearGradient(this.coords.x1,this.coords.y1,this.coords.x2,this.coords.y2):this.type==="radial"&&(t=e.createRadialGradient(this.coords.x1,this.coords.y1,this.coords.r1,this.coords.x2,this.coords.y2,this.coords.r2));for(var n=0,r=this.colorStops.length;n'+''+""},toLive:function(e){var t=typeof this.source=="function"?this.source():this.source;if(typeof t.src!="undefined"){if(!t.complete)return"";if(t.naturalWidth===0||t.naturalHeight===0)return""}return e.createPattern(t,this.repeat)}}),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Shadow){t.warn("fabric.Shadow is already defined.");return}t.Shadow=t.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,initialize:function(e){typeof e=="string"&&(e=this._parseShadow(e));for(var n in e)this[n]=e[n];this.id=t.Object.__uid++},_parseShadow:function(e){var n=e.trim(),r=t.Shadow.reOffsetsAndBlur.exec(n)||[],i=n.replace(t.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)";return{color:i.trim(),offsetX:parseInt(r[1],10)||0,offsetY:parseInt(r[2],10)||0,blur:parseInt(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(e){var t="SourceAlpha";return e&&(e.fill===this.color||e.stroke===this.color)&&(t="SourceGraphic"),''+''+''+""+""+''+""+""},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY};var e={},n=t.Shadow.prototype;return this.color!==n.color&&(e.color=this.color),this.blur!==n.blur&&(e.blur=this.blur),this.offsetX!==n.offsetX&&(e.offsetX=this.offsetX),this.offsetY!==n.offsetY&&(e.offsetY=this.offsetY),e}}),t.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/}(typeof exports!="undefined"?exports:this),function(){"use strict";if(fabric.StaticCanvas){fabric.warn("fabric.StaticCanvas is already defined.");return}var e=fabric.util.object.extend,t=fabric.util.getElementOffset,n=fabric.util.removeFromArray,r=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass({initialize:function(e,t){t||(t={}),this._initStatic(e,t),fabric.StaticCanvas.activeInstance=this},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!0,renderOnAddRemove:!0,clipTo:null,controlsAboveOverlay:!1,allowTouchScrolling:!1,onBeforeScaleRotate:function(){},_initStatic:function(e,t){this._objects=[],this._createLowerCanvas(e),this._initOptions(t),t.overlayImage&&this.setOverlayImage(t.overlayImage,this.renderAll.bind(this)),t.backgroundImage&&this.setBackgroundImage(t.backgroundImage,this.renderAll.bind(this)),t.backgroundColor&&this.setBackgroundColor(t.backgroundColor,this.renderAll.bind(this)),t.overlayColor&&this.setOverlayColor(t.overlayColor,this.renderAll.bind(this)),this.calcOffset()},calcOffset:function(){return this._offset=t(this.lowerCanvasEl),this},setOverlayImage:function(e,t,n){return this.__setBgOverlayImage("overlayImage",e,t,n)},setBackgroundImage:function(e,t,n){return this.__setBgOverlayImage("backgroundImage",e,t,n)},setOverlayColor:function(e,t){return this.__setBgOverlayColor("overlayColor",e,t)},setBackgroundColor:function(e,t){return this.__setBgOverlayColor("backgroundColor",e,t)},__setBgOverlayImage:function(e,t,n,r){return typeof t=="string"?fabric.util.loadImage(t,function(t){this[e]=new fabric.Image(t,r),n&&n()},this):(this[e]=t,n&&n()),this},__setBgOverlayColor:function(e,t,n){if(t.source){var r=this;fabric.util.loadImage(t.source,function(i){r[e]=new fabric.Pattern({source:i,repeat:t.repeat,offsetX:t.offsetX,offsetY:t.offsetY}),n&&n()})}else this[e]=t,n&&n();return this},_createCanvasElement:function(){var e=fabric.document.createElement("canvas");e.style||(e.style={});if(!e)throw r;return this._initCanvasElement(e),e},_initCanvasElement:function(e){fabric.util.createCanvasElement(e);if(typeof e.getContext=="undefined")throw r},_initOptions:function(e){for(var t in e)this[t]=e[t];this.width=parseInt(this.lowerCanvasEl.width,10)||0,this.height=parseInt(this.lowerCanvasEl.height,10)||0;if(!this.lowerCanvasEl.style)return;this.lowerCanvasEl.style.width=this.width+"px",this.lowerCanvasEl.style.height=this.height+"px"},_createLowerCanvas:function(e){this.lowerCanvasEl=fabric.util.getById(e)||this._createCanvasElement(),this._initCanvasElement(this.lowerCanvasEl),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(e){return this._setDimension("width",e)},setHeight:function(e){return this._setDimension("height",e)},setDimensions:function(e){for(var t in e)this._setDimension(t,e[t]);return this},_setDimension:function(e,t){return this.lowerCanvasEl[e]=t,this.lowerCanvasEl.style[e]=t+"px",this.upperCanvasEl&&(this.upperCanvasEl[e]=t,this.upperCanvasEl.style[e]=t+"px"),this.cacheCanvasEl&&(this.cacheCanvasEl[e]=t),this.wrapperEl&&(this.wrapperEl.style[e]=t+"px"),this[e]=t,this.calcOffset(),this.renderAll(),this},getElement:function(){return this.lowerCanvasEl},getActiveObject:function(){return null},getActiveGroup:function(){return null},_draw:function(e,t){if(!t)return;if(this.controlsAboveOverlay){var n=t.hasBorders,r=t.hasControls;t.hasBorders=t.hasControls=!1,t.render(e),t.hasBorders=n,t.hasControls=r}else t.render(e)},_onObjectAdded:function(e){this.stateful&&e.setupState(),e.setCoords(),e.canvas=this,this.fire("object:added",{target:e}),e.fire("added")},_onObjectRemoved:function(e){this.getActiveObject()===e&&(this.fire("before:selection:cleared",{target:e}),this._discardActiveObject(),this.fire("selection:cleared")),this.fire("object:removed",{target:e}),e.fire("removed")},clearContext:function(e){return e.clearRect(0,0,this.width,this.height),this},getContext:function(){return this.contextContainer},clear:function(){return this._objects.length=0,this.discardActiveGroup&&this.discardActiveGroup(),this.discardActiveObject&&this.discardActiveObject(),this.clearContext(this.contextContainer),this.contextTop&&this.clearContext(this.contextTop),this.fire("canvas:cleared"),this.renderAll(),this},renderAll:function(e){var t=this[e===!0&&this.interactive?"contextTop":"contextContainer"],n=this.getActiveGroup();return this.contextTop&&this.selection&&!this._groupSelector&&this.clearContext(this.contextTop),e||this.clearContext(t),this.fire("before:render"),this.clipTo&&fabric.util.clipContext(this,t),this._renderBackground(t),this._renderObjects(t,n),this._renderActiveGroup(t,n),this.clipTo&&t.restore(),this._renderOverlay(t),this.controlsAboveOverlay&&this.interactive&&this.drawControls(t),this.fire("after:render"),this},_renderObjects:function(e,t){for(var n=0,r=this._objects.length;n"),n.join("")},_setSVGPreamble:function(e,t){t.suppressPreamble||e.push('','\n')},_setSVGHeader:function(e,t){e.push("',"Created with Fabric.js ",fabric.version,"","",fabric.createSVGFontFacesMarkup(this.getObjects()),fabric.createSVGRefElementsMarkup(this),"")},_setSVGObjects:function(e,t){var n=this.getActiveGroup();n&&this.discardActiveGroup();for(var r=0,i=this.getObjects(),s=i.length;r"):this[t]&&t==="overlayColor"&&e.push('")},sendToBack:function(e){return n(this._objects,e),this._objects.unshift(e),this.renderAll&&this.renderAll()},bringToFront:function(e){return n(this._objects,e),this._objects.push(e),this.renderAll&&this.renderAll()},sendBackwards:function(e,t){var r=this._objects.indexOf(e);if(r!==0){var i=this._findNewLowerIndex(e,r,t);n(this._objects,e),this._objects.splice(i,0,e),this.renderAll&&this.renderAll()}return this},_findNewLowerIndex:function(e,t,n){var r;if(n){r=t;for(var i=t-1;i>=0;--i){var s=e.intersectsWithObject(this._objects[i])||e.isContainedWithinObject(this._objects[i])||this._objects[i].isContainedWithinObject(e);if(s){r=i;break}}}else r=t-1;return r},bringForward:function(e,t){var r=this._objects.indexOf(e);if(r!==this._objects.length-1){var i=this._findNewUpperIndex(e,r,t);n(this._objects,e),this._objects.splice(i,0,e),this.renderAll&&this.renderAll()}return this},_findNewUpperIndex:function(e,t,n){var r;if(n){r=t;for(var i=t+1;i"}}),e(fabric.StaticCanvas.prototype,fabric.Observable),e(fabric.StaticCanvas.prototype,fabric.Collection),e(fabric.StaticCanvas.prototype,fabric.DataURLExporter),e(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(e){var t=fabric.util.createCanvasElement();if(!t||!t.getContext)return null;var n=t.getContext("2d");if(!n)return null;switch(e){case"getImageData":return typeof n.getImageData!="undefined";case"setLineDash":return typeof n.setLineDash!="undefined";case"toDataURL":return typeof t.toDataURL!="undefined";case"toDataURLWithQuality":try{return t.toDataURL("image/jpeg",0),!0}catch(r){}return!1;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",setShadow:function(e){return this.shadow=new fabric.Shadow(e),this},_setBrushStyles:function(){var e=this.canvas.contextTop;e.strokeStyle=this.color,e.lineWidth=this.width,e.lineCap=this.strokeLineCap,e.lineJoin=this.strokeLineJoin},_setShadow:function(){if(!this.shadow)return;var e=this.canvas.contextTop;e.shadowColor=this.shadow.color,e.shadowBlur=this.shadow.blur,e.shadowOffsetX=this.shadow.offsetX,e.shadowOffsetY=this.shadow.offsetY},_resetShadow:function(){var e=this.canvas.contextTop;e.shadowColor="",e.shadowBlur=e.shadowOffsetX=e.shadowOffsetY=0}}),function(){var e=fabric.util.array.min,t=fabric.util.array.max;fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{initialize:function(e){this.canvas=e,this._points=[]},onMouseDown:function(e){this._prepareForDrawing(e),this._captureDrawingPath(e),this._render()},onMouseMove:function(e){this._captureDrawingPath(e),this.canvas.clearContext(this.canvas.contextTop),this._render()},onMouseUp:function(){this._finalizeAndAddPath()},_prepareForDrawing:function(e){var t=new fabric.Point(e.x,e.y);this._reset(),this._addPoint(t),this.canvas.contextTop.moveTo(t.x,t.y)},_addPoint:function(e){this._points.push(e)},_reset:function(){this._points.length=0,this._setBrushStyles(),this._setShadow()},_captureDrawingPath:function(e){var t=new fabric.Point(e.x,e.y);this._addPoint(t)},_render:function(){var e=this.canvas.contextTop;e.beginPath();var t=this._points[0],n=this._points[1];this._points.length===2&&t.x===n.x&&t.y===n.y&&(t.x-=.5,n.x+=.5),e.moveTo(t.x,t.y);for(var r=1,i=this._points.length;rn.padding?e.x<0?e.x+=n.padding:e.x-=n.padding:e.x=0,i(e.y)>n.padding?e.y<0?e.y+=n.padding:e.y-=n.padding:e.y=0},_rotateObject:function(e,t){var i=this._currentTransform,s=this._offset;if(i.target.get("lockRotation"))return;var o=r(i.ey-i.top-s.top,i.ex-i.left-s.left),u=r(t-i.top-s.top,e-i.left-s.left),a=n(u-o+i.theta);a<0&&(a=360+a),i.target.angle=a},_setCursor:function(e){this.upperCanvasEl.style.cursor=e},_resetObjectTransform:function(e){e.scaleX=1,e.scaleY=1,e.setAngle(0)},_drawSelection:function(){var e=this.contextTop,t=this._groupSelector,n=t.left,r=t.top,o=i(n),u=i(r);e.fillStyle=this.selectionColor,e.fillRect(t.ex-(n>0?0:-n),t.ey-(r>0?0:-r),o,u),e.lineWidth=this.selectionLineWidth,e.strokeStyle=this.selectionBorderColor;if(this.selectionDashArray.length>1){var a=t.ex+s-(n>0?0:o),f=t.ey+s-(r>0?0:u);e.beginPath(),fabric.util.drawDashedLine(e,a,f,a+o,f,this.selectionDashArray),fabric.util.drawDashedLine(e,a,f+u-1,a+o,f+u-1,this.selectionDashArray),fabric.util.drawDashedLine(e,a,f,a,f+u,this.selectionDashArray),fabric.util.drawDashedLine(e,a+o-1,f,a+o-1,f+u,this.selectionDashArray),e.closePath(),e.stroke()}else e.strokeRect(t.ex+s-(n>0?0:o),t.ey+s-(r>0?0:u),o,u)},_isLastRenderedObject:function(e){return this.controlsAboveOverlay&&this.lastRenderedObjectWithControlsAboveOverlay&&this.lastRenderedObjectWithControlsAboveOverlay.visible&&this.containsPoint(e,this.lastRenderedObjectWithControlsAboveOverlay)&&this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e,this._offset)},findTarget:function(e,t){if(this.skipTargetFind)return;if(this._isLastRenderedObject(e))return this.lastRenderedObjectWithControlsAboveOverlay;var n=this.getActiveGroup();return n&&!t&&this.containsPoint(e,n)?n:this._searchPossibleTargets(e)},_searchPossibleTargets:function(e){var t=[],n,r=this.getPointer(e);for(var i=this._objects.length;i--;)if(this._objects[i]&&this._objects[i].visible&&this._objects[i].evented&&this.containsPoint(e,this._objects[i])){if(!this.perPixelTargetFind&&!this._objects[i].perPixelTargetFind){n=this._objects[i],this.relatedTarget=n;break}t[t.length]=this._objects[i]}for(var s=0,o=t.length;s1&&(t=new fabric.Group(t.reverse(),{originX:"center",originY:"center"}),this.setActiveGroup(t,e),t.saveCoords(),this.fire("selection:created",{target:t}),this.renderAll())},_collectObjects:function(){var n=[],r,i=this._groupSelector.ex,s=this._groupSelector.ey,o=i+this._groupSelector.left,u=s+this._groupSelector.top,a=new fabric.Point(e(i,o),e(s,u)),f=new fabric.Point(t(i,o),t(s,u)),l=i===o&&s===u;for(var c=this._objects.length;c--;){r=this._objects[c];if(!r||!r.selectable||!r.visible)continue;if(r.intersectsWithRect(a,f)||r.isContainedWithinRect(a,f)||r.containsPoint(a)||r.containsPoint(f)){r.set("active",!0),n.push(r);if(l)break}}return n},_maybeGroupObjects:function(e){this.selection&&this._groupSelector&&this._groupSelectedObjects(e);var t=this.getActiveGroup();t&&(t.setObjectsCoords().setCoords(),t.isMoving=!1,this._setCursor(this.defaultCursor)),this._groupSelector=null,this._currentTransform=null}})}(),fabric.util.object.extend(fabric.StaticCanvas.prototype,{toDataURL:function(e){e||(e={});var t=e.format||"png",n=e.quality||1,r=e.multiplier||1,i={left:e.left,top:e.top,width:e.width,height:e.height};return r!==1?this.__toDataURLWithMultiplier(t,n,i,r):this.__toDataURL(t,n,i)},__toDataURL:function(e,t,n){this.renderAll(!0);var r=this.upperCanvasEl||this.lowerCanvasEl,i=this.__getCroppedCanvas(r,n);e==="jpg"&&(e="jpeg");var s=fabric.StaticCanvas.supports("toDataURLWithQuality")?(i||r).toDataURL("image/"+e,t):(i||r).toDataURL("image/"+e);return this.contextTop&&this.clearContext(this.contextTop),this.renderAll(),i&&(i=null),s},__getCroppedCanvas:function(e,t){var n,r,i="left"in t||"top"in t||"width"in t||"height"in t;return i&&(n=fabric.util.createCanvasElement(),r=n.getContext("2d"),n.width=t.width||this.width,n.height=t.height||this.height,r.drawImage(e,-t.left||0,-t.top||0)),n},__toDataURLWithMultiplier:function(e,t,n,r){var i=this.getWidth(),s=this.getHeight(),o=i*r,u=s*r,a=this.getActiveObject(),f=this.getActiveGroup(),l=this.contextTop||this.contextContainer;this.setWidth(o).setHeight(u),l.scale(r,r),n.left&&(n.left*=r),n.top&&(n.top*=r),n.width&&(n.width*=r),n.height&&(n.height*=r),f?this._tempRemoveBordersControlsFromGroup(f):a&&this.deactivateAll&&this.deactivateAll(),this.renderAll(!0);var c=this.__toDataURL(e,t,n);return this.width=i,this.height=s,l.scale(1/r,1/r),this.setWidth(i).setHeight(s),f?this._restoreBordersControlsOnGroup(f):a&&this.setActiveObject&&this.setActiveObject(a),this.contextTop&&this.clearContext(this.contextTop),this.renderAll(),c},toDataURLWithMultiplier:function(e,t,n){return this.toDataURL({format:e,multiplier:t,quality:n})},_tempRemoveBordersControlsFromGroup:function(e){e.origHasControls=e.hasControls,e.origBorderColor=e.borderColor,e.hasControls=!0,e.borderColor="rgba(0,0,0,0)",e.forEachObject(function(e){e.origBorderColor=e.borderColor,e.borderColor="rgba(0,0,0,0)"})},_restoreBordersControlsOnGroup:function(e){e.hideControls=e.origHideControls,e.borderColor=e.origBorderColor,e.forEachObject(function(e){e.borderColor=e.origBorderColor,delete e.origBorderColor})}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{loadFromDatalessJSON:function(e,t,n){return this.loadFromJSON(e,t,n)},loadFromJSON:function(e,t,n){if(!e)return;var r=typeof e=="string"?JSON.parse(e):e;this.clear();var i=this;return this._enlivenObjects(r.objects,function(){i._setBgOverlay(r,t)},n),this},_setBgOverlay:function(e,t){var n=this,r={backgroundColor:!1,overlayColor:!1,backgroundImage:!1,overlayImage:!1};if(!e.backgroundImage&&!e.overlayImage&&!e.background&&!e.overlay){t&&t();return}var i=function(){r.backgroundImage&&r.overlayImage&&r.backgroundColor&&r.overlayColor&&(n.renderAll(),t&&t())};this.__setBgOverlay("backgroundImage",e.backgroundImage,r,i),this.__setBgOverlay("overlayImage",e.overlayImage,r,i),this.__setBgOverlay("backgroundColor",e.background,r,i),this.__setBgOverlay("overlayColor",e.overlay,r,i),i()},__setBgOverlay:function(e,t,n,r){var i=this;if(!t){n[e]=!0;return}e==="backgroundImage"||e==="overlayImage"?fabric.Image.fromObject(t,function(t){i[e]=t,n[e]=!0,r&&r()}):this["set"+fabric.util.string.capitalize(e,!0)](t,function(){n[e]=!0,r&&r()})},_enlivenObjects:function(e,t,n){var r=this;e.length===0&&t&&t();var i=this.renderOnAddRemove;this.renderOnAddRemove=!1,fabric.util.enlivenObjects(e,function(e){e.forEach(function(e,t){r.insertAt(e,t,!0)}),r.renderOnAddRemove=i,t&&t()},null,n)},_toDataURL:function(e,t){this.clone(function(n){t(n.toDataURL(e))})},_toDataURLWithMultiplier:function(e,t,n){this.clone(function(r){n(r.toDataURLWithMultiplier(e,t))})},clone:function(e,t){var n=JSON.stringify(this.toJSON(t));this.cloneWithoutData(function(t){t.loadFromJSON(n,function(){e&&e(t)})})},cloneWithoutData:function(e){var t=fabric.document.createElement("canvas");t.width=this.getWidth(),t.height=this.getHeight();var n=new fabric.Canvas(t);n.clipTo=this.clipTo,this.backgroundImage?(n.setBackgroundImage(this.backgroundImage.src,function(){n.renderAll(),e&&e(n)}),n.backgroundImageOpacity=this.backgroundImageOpacity,n.backgroundImageStretch=this.backgroundImageStretch):e&&e(n)}}),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.toFixed,i=t.util.string.capitalize,s=t.util.degreesToRadians,o=t.StaticCanvas.supports("setLineDash");if(t.Object)return;t.Object=t.util.createClass({type:"object",originX:"left",originY:"top",top:0,left:0,width:0,height:0,scaleX:1,scaleY:1,flipX:!1,flipY:!1,opacity:1,angle:0,cornerSize:12,transparentCorners:!0,hoverCursor:null,padding:0,borderColor:"rgba(102,153,255,0.75)",cornerColor:"rgba(102,153,255,0.5)",centeredScaling:!1,centeredRotation:!0,fill:"rgb(0,0,0)",fillRule:"source-over",backgroundColor:"",stroke:null,strokeWidth:1,strokeDashArray:null,strokeLineCap:"butt",strokeLineJoin:"miter",strokeMiterLimit:10,shadow:null,borderOpacityWhenMoving:.4,borderScaleFactor:1,transformMatrix:null,minScaleLimit:.01,selectable:!0,evented:!0,visible:!0,hasControls:!0,hasBorders:!0,hasRotatingPoint:!0,rotatingPointOffset:40,perPixelTargetFind:!1,includeDefaultValues:!0,clipTo:null,lockMovementX:!1,lockMovementY:!1,lockRotation:!1,lockScalingX:!1,lockScalingY:!1,lockUniScaling:!1,stateProperties:"top left width height scaleX scaleY flipX flipY originX originY transformMatrix stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit angle opacity fill fillRule shadow clipTo visible backgroundColor".split(" "),initialize:function(e){e&&this.setOptions(e)},_initGradient:function(e){e.fill&&e.fill.colorStops&&!(e.fill instanceof t.Gradient)&&this.set("fill",new t.Gradient(e.fill))},_initPattern:function(e){e.fill&&e.fill.source&&!(e.fill instanceof t.Pattern)&&this.set("fill",new t.Pattern(e.fill)),e.stroke&&e.stroke.source&&!(e.stroke instanceof t.Pattern)&&this.set("stroke",new t.Pattern(e.stroke))},_initClipping:function(e){if(!e.clipTo||typeof e.clipTo!="string")return;var n=t.util.getFunctionBody(e.clipTo);typeof n!="undefined"&&(this.clipTo=new Function("ctx",n))},setOptions:function(e){for(var t in e)this.set(t,e[t]);this._initGradient(e),this._initPattern(e),this._initClipping(e)},transform:function(e,t){e.globalAlpha=this.opacity;var n=t?this._getLeftTopCoords():this.getCenterPoint();e.translate(n.x,n.y),e.rotate(s(this.angle)),e.scale(this.scaleX*(this.flipX?-1:1),this.scaleY*(this.flipY?-1:1))},toObject:function(e){var n=t.Object.NUM_FRACTION_DIGITS,i={type:this.type,originX:this.originX,originY:this.originY,left:r(this.left,n),top:r(this.top,n),width:r(this.width,n),height:r(this.height,n),fill:this.fill&&this.fill.toObject?this.fill.toObject():this.fill,stroke:this.stroke&&this.stroke.toObject?this.stroke.toObject():this.stroke,strokeWidth:r(this.strokeWidth,n),strokeDashArray:this.strokeDashArray,strokeLineCap:this.strokeLineCap,strokeLineJoin:this.strokeLineJoin,strokeMiterLimit:r(this.strokeMiterLimit,n),scaleX:r(this.scaleX,n),scaleY:r(this.scaleY,n),angle:r(this.getAngle(),n),flipX:this.flipX,flipY:this.flipY,opacity:r(this.opacity,n),shadow:this.shadow&&this.shadow.toObject?this.shadow.toObject():this.shadow,visible:this.visible,clipTo:this.clipTo&&String(this.clipTo),backgroundColor:this.backgroundColor};return this.includeDefaultValues||(i=this._removeDefaultValues(i)),t.util.populateWithProperties(this,i,e),i},toDatalessObject:function(e){return this.toObject(e)},_removeDefaultValues:function(e){var n=t.util.getKlass(e.type).prototype,r=n.stateProperties;return r.forEach(function(t){e[t]===n[t]&&delete e[t]}),e},toString:function(){return"#"},get:function(e){return this[e]},set:function(e,t){if(typeof e=="object")for(var n in e)this._set(n,e[n]);else typeof t=="function"&&e!=="clipTo"?this._set(e,t(this.get(e))):this._set(e,t);return this},_set:function(e,n){var i=e==="scaleX"||e==="scaleY";return i&&(n=this._constrainScale(n)),e==="scaleX"&&n<0?(this.flipX=!this.flipX,n*=-1):e==="scaleY"&&n<0?(this.flipY=!this.flipY,n*=-1):e==="width"||e==="height"?this.minScaleLimit=r(Math.min(.1,1/Math.max(this.width,this.height)),2):e==="shadow"&&n&&!(n instanceof t.Shadow)&&(n=new t.Shadow(n)),this[e +]=n,this},toggle:function(e){var t=this.get(e);return typeof t=="boolean"&&this.set(e,!t),this},setSourcePath:function(e){return this.sourcePath=e,this},render:function(e,n){if(this.width===0||this.height===0||!this.visible)return;e.save(),this._transform(e,n),this._setStrokeStyles(e),this._setFillStyles(e);var r=this.transformMatrix;r&&this.group&&(e.translate(-this.group.width/2,-this.group.height/2),e.transform(r[0],r[1],r[2],r[3],r[4],r[5])),this._setShadow(e),this.clipTo&&t.util.clipContext(this,e),this._render(e,n),this.clipTo&&e.restore(),this._removeShadow(e),this.active&&!n&&(this.drawBorders(e),this.drawControls(e)),e.restore()},_transform:function(e,t){var n=this.transformMatrix;n&&!this.group&&e.setTransform(n[0],n[1],n[2],n[3],n[4],n[5]),t||this.transform(e)},_setStrokeStyles:function(e){this.stroke&&(e.lineWidth=this.strokeWidth,e.lineCap=this.strokeLineCap,e.lineJoin=this.strokeLineJoin,e.miterLimit=this.strokeMiterLimit,e.strokeStyle=this.stroke.toLive?this.stroke.toLive(e):this.stroke)},_setFillStyles:function(e){this.fill&&(e.fillStyle=this.fill.toLive?this.fill.toLive(e):this.fill)},_setShadow:function(e){if(!this.shadow)return;e.shadowColor=this.shadow.color,e.shadowBlur=this.shadow.blur,e.shadowOffsetX=this.shadow.offsetX,e.shadowOffsetY=this.shadow.offsetY},_removeShadow:function(e){e.shadowColor="",e.shadowBlur=e.shadowOffsetX=e.shadowOffsetY=0},_renderFill:function(e){if(!this.fill)return;this.fill.toLive&&(e.save(),e.translate(-this.width/2+this.fill.offsetX||0,-this.height/2+this.fill.offsetY||0)),e.fill(),this.fill.toLive&&e.restore(),this.shadow&&!this.shadow.affectStroke&&this._removeShadow(e)},_renderStroke:function(e){if(!this.stroke)return;e.save(),this.strokeDashArray?(1&this.strokeDashArray.length&&this.strokeDashArray.push.apply(this.strokeDashArray,this.strokeDashArray),o?(e.setLineDash(this.strokeDashArray),this._stroke&&this._stroke(e)):this._renderDashedStroke&&this._renderDashedStroke(e),e.stroke()):this._stroke?this._stroke(e):e.stroke(),this._removeShadow(e),e.restore()},clone:function(e,n){return this.constructor.fromObject?this.constructor.fromObject(this.toObject(n),e):new t.Object(this.toObject(n))},cloneAsImage:function(e){var n=this.toDataURL();return t.util.loadImage(n,function(n){e&&e(new t.Image(n))}),this},toDataURL:function(e){e||(e={});var n=t.util.createCanvasElement(),r=this.getBoundingRect();n.width=r.width,n.height=r.height,t.util.wrapElement(n,"div");var i=new t.Canvas(n);e.format==="jpg"&&(e.format="jpeg"),e.format==="jpeg"&&(i.backgroundColor="#fff");var s={active:this.get("active"),left:this.getLeft(),top:this.getTop()};this.set("active",!1),this.setPositionByOrigin(new t.Point(n.width/2,n.height/2),"center","center");var o=this.canvas;i.add(this);var u=i.toDataURL(e);return this.set(s).setCoords(),this.canvas=o,i.dispose(),i=null,u},isType:function(e){return this.type===e},complexity:function(){return 0},toJSON:function(e){return this.toObject(e)},setGradient:function(e,n){n||(n={});var r={colorStops:[]};r.type=n.type||(n.r1||n.r2?"radial":"linear"),r.coords={x1:n.x1,y1:n.y1,x2:n.x2,y2:n.y2};if(n.r1||n.r2)r.coords.r1=n.r1,r.coords.r2=n.r2;for(var i in n.colorStops){var s=new t.Color(n.colorStops[i]);r.colorStops.push({offset:i,color:s.toRgb(),opacity:s.getAlpha()})}return this.set(e,t.Gradient.forObject(this,r))},setPatternFill:function(e){return this.set("fill",new t.Pattern(e))},setShadow:function(e){return this.set("shadow",new t.Shadow(e))},setColor:function(e){return this.set("fill",e),this},centerH:function(){return this.canvas.centerObjectH(this),this},centerV:function(){return this.canvas.centerObjectV(this),this},center:function(){return this.canvas.centerObject(this),this},remove:function(){return this.canvas.remove(this)},getLocalPointer:function(e,t){t=t||this.canvas.getPointer(e);var n=this.translateToOriginPoint(this.getCenterPoint(),"left","top");return{x:t.x-n.x,y:t.y-n.y}}}),t.util.createAccessors(t.Object),t.Object.prototype.rotate=t.Object.prototype.setAngle,n(t.Object.prototype,t.Observable),t.Object.NUM_FRACTION_DIGITS=2,t.Object.__uid=0}(typeof exports!="undefined"?exports:this),function(){var e=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{translateToCenterPoint:function(t,n,r){var i=t.x,s=t.y,o=this.stroke?this.strokeWidth:0;return n==="left"?i=t.x+(this.getWidth()+o*this.scaleX)/2:n==="right"&&(i=t.x-(this.getWidth()+o*this.scaleX)/2),r==="top"?s=t.y+(this.getHeight()+o*this.scaleY)/2:r==="bottom"&&(s=t.y-(this.getHeight()+o*this.scaleY)/2),fabric.util.rotatePoint(new fabric.Point(i,s),t,e(this.angle))},translateToOriginPoint:function(t,n,r){var i=t.x,s=t.y,o=this.stroke?this.strokeWidth:0;return n==="left"?i=t.x-(this.getWidth()+o*this.scaleX)/2:n==="right"&&(i=t.x+(this.getWidth()+o*this.scaleX)/2),r==="top"?s=t.y-(this.getHeight()+o*this.scaleY)/2:r==="bottom"&&(s=t.y+(this.getHeight()+o*this.scaleY)/2),fabric.util.rotatePoint(new fabric.Point(i,s),t,e(this.angle))},getCenterPoint:function(){var e=new fabric.Point(this.left,this.top);return this.translateToCenterPoint(e,this.originX,this.originY)},getPointByOrigin:function(e,t){var n=this.getCenterPoint();return this.translateToOriginPoint(n,e,t)},toLocalPoint:function(t,n,r){var i=this.getCenterPoint(),s=this.stroke?this.strokeWidth:0,o,u;return n&&r?(n==="left"?o=i.x-(this.getWidth()+s*this.scaleX)/2:n==="right"?o=i.x+(this.getWidth()+s*this.scaleX)/2:o=i.x,r==="top"?u=i.y-(this.getHeight()+s*this.scaleY)/2:r==="bottom"?u=i.y+(this.getHeight()+s*this.scaleY)/2:u=i.y):(o=this.left,u=this.top),fabric.util.rotatePoint(new fabric.Point(t.x,t.y),i,-e(this.angle)).subtractEquals(new fabric.Point(o,u))},setPositionByOrigin:function(e,t,n){var r=this.translateToCenterPoint(e,t,n),i=this.translateToOriginPoint(r,this.originX,this.originY);this.set("left",i.x),this.set("top",i.y)},adjustPosition:function(t){var n=e(this.angle),r=this.getWidth()/2,i=Math.cos(n)*r,s=Math.sin(n)*r,o=this.getWidth(),u=Math.cos(n)*o,a=Math.sin(n)*o;this.originX==="center"&&t==="left"||this.originX==="right"&&t==="center"?(this.left-=i,this.top-=s):this.originX==="left"&&t==="center"||this.originX==="center"&&t==="right"?(this.left+=i,this.top+=s):this.originX==="left"&&t==="right"?(this.left+=u,this.top+=a):this.originX==="right"&&t==="left"&&(this.left-=u,this.top-=a),this.setCoords(),this.originX=t},_getLeftTopCoords:function(){return this.translateToOriginPoint(this.getCenterPoint(),"left","center")}})}(),function(){var e=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{oCoords:null,intersectsWithRect:function(e,t){var n=this.oCoords,r=new fabric.Point(n.tl.x,n.tl.y),i=new fabric.Point(n.tr.x,n.tr.y),s=new fabric.Point(n.bl.x,n.bl.y),o=new fabric.Point(n.br.x,n.br.y),u=fabric.Intersection.intersectPolygonRectangle([r,i,o,s],e,t);return u.status==="Intersection"},intersectsWithObject:function(e){function t(e){return{tl:new fabric.Point(e.tl.x,e.tl.y),tr:new fabric.Point(e.tr.x,e.tr.y),bl:new fabric.Point(e.bl.x,e.bl.y),br:new fabric.Point(e.br.x,e.br.y)}}var n=t(this.oCoords),r=t(e.oCoords),i=fabric.Intersection.intersectPolygonPolygon([n.tl,n.tr,n.br,n.bl],[r.tl,r.tr,r.br,r.bl]);return i.status==="Intersection"},isContainedWithinObject:function(e){var t=e.getBoundingRect(),n=new fabric.Point(t.left,t.top),r=new fabric.Point(t.left+t.width,t.top+t.height);return this.isContainedWithinRect(n,r)},isContainedWithinRect:function(e,t){var n=this.getBoundingRect();return n.left>e.x&&n.left+n.widthe.y&&n.top+n.height=e.y&&f.d.y>=e.y)continue;f.o.x===f.d.x&&f.o.x>=e.x?(o=f.o.x,u=e.y):(n=0,r=(f.d.y-f.o.y)/(f.d.x-f.o.x),i=e.y-n*e.x,s=f.o.y-r*f.o.x,o=-(i-s)/(n-r),u=i+n*o),o>=e.x&&(a+=1);if(a===2)break}return a},getBoundingRectWidth:function(){return this.getBoundingRect().width},getBoundingRectHeight:function(){return this.getBoundingRect().height},getBoundingRect:function(){this.oCoords||this.setCoords();var e=[this.oCoords.tl.x,this.oCoords.tr.x,this.oCoords.br.x,this.oCoords.bl.x],t=fabric.util.array.min(e),n=fabric.util.array.max(e),r=Math.abs(t-n),i=[this.oCoords.tl.y,this.oCoords.tr.y,this.oCoords.br.y,this.oCoords.bl.y],s=fabric.util.array.min(i),o=fabric.util.array.max(i),u=Math.abs(s-o);return{left:t,top:s,width:r,height:u}},getWidth:function(){return this.width*this.scaleX},getHeight:function(){return this.height*this.scaleY},_constrainScale:function(e){return Math.abs(e)1?this.strokeWidth:0,n=this.padding,r=e(this.angle);this.currentWidth=(this.width+t)*this.scaleX+n*2,this.currentHeight=(this.height+t)*this.scaleY+n*2,this.currentWidth<0&&(this.currentWidth=Math.abs(this.currentWidth));var i=Math.sqrt(Math.pow(this.currentWidth/2,2)+Math.pow(this.currentHeight/2,2)),s=Math.atan(isFinite(this.currentHeight/this.currentWidth)?this.currentHeight/this.currentWidth:0),o=Math.cos(s+r)*i,u=Math.sin(s+r)*i,a=Math.sin(r),f=Math.cos(r),l=this.getCenterPoint(),c={x:l.x-o,y:l.y-u},h={x:c.x+this.currentWidth*f,y:c.y+this.currentWidth*a},p={x:h.x-this.currentHeight*a,y:h.y+this.currentHeight*f},d={x:c.x-this.currentHeight*a,y:c.y+this.currentHeight*f},v={x:c.x-this.currentHeight/2*a,y:c.y+this.currentHeight/2*f},m={x:c.x+this.currentWidth/2*f,y:c.y+this.currentWidth/2*a},g={x:h.x-this.currentHeight/2*a,y:h.y+this.currentHeight/2*f},y={x:d.x+this.currentWidth/2*f,y:d.y+this.currentWidth/2*a},b={x:m.x,y:m.y};return this.oCoords={tl:c,tr:h,br:p,bl:d,ml:v,mt:m,mr:g,mb:y,mtr:b},this._setCornerCoords&&this._setCornerCoords(),this}})}(),fabric.util.object.extend(fabric.Object.prototype,{sendToBack:function(){return this.group?fabric.StaticCanvas.prototype.sendToBack.call(this.group,this):this.canvas.sendToBack(this),this},bringToFront:function(){return this.group?fabric.StaticCanvas.prototype.bringToFront.call(this.group,this):this.canvas.bringToFront(this),this},sendBackwards:function(e){return this.group?fabric.StaticCanvas.prototype.sendBackwards.call(this.group,this,e):this.canvas.sendBackwards(this,e),this},bringForward:function(e){return this.group?fabric.StaticCanvas.prototype.bringForward.call(this.group,this,e):this.canvas.bringForward(this,e),this},moveTo:function(e){return this.group?fabric.StaticCanvas.prototype.moveTo.call(this.group,this,e):this.canvas.moveTo(this,e),this}}),fabric.util.object.extend(fabric.Object.prototype,{getSvgStyles:function(){var e=this.fill?this.fill.toLive?"url(#SVGID_"+this.fill.id+")":this.fill:"none",t=this.stroke?this.stroke.toLive?"url(#SVGID_"+this.stroke.id+")":this.stroke:"none",n=this.strokeWidth?this.strokeWidth:"0",r=this.strokeDashArray?this.strokeDashArray.join(" "):"",i=this.strokeLineCap?this.strokeLineCap:"butt",s=this.strokeLineJoin?this.strokeLineJoin:"miter",o=this.strokeMiterLimit?this.strokeMiterLimit:"4",u=typeof this.opacity!="undefined"?this.opacity:"1",a=this.visible?"":" visibility: hidden;",f=this.shadow&&this.type!=="text"?"filter: url(#SVGID_"+this.shadow.id+");":"";return["stroke: ",t,"; ","stroke-width: ",n,"; ","stroke-dasharray: ",r,"; ","stroke-linecap: ",i,"; ","stroke-linejoin: ",s,"; ","stroke-miterlimit: ",o,"; ","fill: ",e,"; ","opacity: ",u,";",f,a].join("")},getSvgTransform:function(){var e=fabric.util.toFixed,t=this.getAngle(),n=this.getCenterPoint(),r=fabric.Object.NUM_FRACTION_DIGITS,i="translate("+e(n.x,r)+" "+e(n.y,r)+")",s=t!==0?" rotate("+e(t,r)+")":"",o=this.scaleX===1&&this.scaleY===1?"":" scale("+e(this.scaleX,r)+" "+e(this.scaleY,r)+")",u=this.flipX?"matrix(-1 0 0 1 0 0) ":"",a=this.flipY?"matrix(1 0 0 -1 0 0)":"";return[i,s,o,u,a].join("")},_createBaseSVGMarkup:function(){var e=[];return this.fill&&this.fill.toLive&&e.push(this.fill.toSVG(this,!1)),this.stroke&&this.stroke.toLive&&e.push(this.stroke.toSVG(this,!1)),this.shadow&&e.push(this.shadow.toSVG(this)),e}}),fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(){return this.stateProperties.some(function(e){return this.get(e)!==this.originalState[e]},this)},saveState:function(e){return this.stateProperties.forEach(function(e){this.originalState[e]=this.get(e)},this),e&&e.stateProperties&&e.stateProperties.forEach(function(e){this.originalState[e]=this.get(e)},this),this},setupState:function(){return this.originalState={},this.saveState(),this}}),function(){var e=fabric.util.getPointer,t=fabric.util.degreesToRadians,n=typeof G_vmlCanvasManager!="undefined";fabric.util.object.extend(fabric.Object.prototype,{_controlsVisibility:null,_findTargetCorner:function(t,n){if(!this.hasControls||!this.active)return!1;var r=e(t,this.canvas.upperCanvasEl),i=r.x-n.left,s=r.y-n.top,o,u;for(var a in this.oCoords){if(!this.isControlVisible(a))continue;if(a==="mtr"&&!this.hasRotatingPoint)continue;if(!(!this.get("lockUniScaling")||a!=="mt"&&a!=="mr"&&a!=="mb"&&a!=="ml"))continue;u=this._getImageLines(this.oCoords[a].corner),o=this._findCrossPoints({x:i,y:s},u);if(o!==0&&o%2===1)return this.__corner=a,a}return!1},_setCornerCoords:function(){var e=this.oCoords,n=t(this.angle),r=t(45-this.angle),i=Math.sqrt(2*Math.pow(this.cornerSize,2))/2,s=i*Math.cos(r),o=i*Math.sin(r),u=Math.sin(n),a=Math.cos(n);e.tl.corner={tl:{x:e.tl.x-o,y:e.tl.y-s},tr:{x:e.tl.x+s,y:e.tl.y-o},bl:{x:e.tl.x-s,y:e.tl.y+o},br:{x:e.tl.x+o,y:e.tl.y+s}},e.tr.corner={tl:{x:e.tr.x-o,y:e.tr.y-s},tr:{x:e.tr.x+s,y:e.tr.y-o},br:{x:e.tr.x+o,y:e.tr.y+s},bl:{x:e.tr.x-s,y:e.tr.y+o}},e.bl.corner={tl:{x:e.bl.x-o,y:e.bl.y-s},bl:{x:e.bl.x-s,y:e.bl.y+o},br:{x:e.bl.x+o,y:e.bl.y+s},tr:{x:e.bl.x+s,y:e.bl.y-o}},e.br.corner={tr:{x:e.br.x+s,y:e.br.y-o},bl:{x:e.br.x-s,y:e.br.y+o},br:{x:e.br.x+o,y:e.br.y+s},tl:{x:e.br.x-o,y:e.br.y-s}},e.ml.corner={tl:{x:e.ml.x-o,y:e.ml.y-s},tr:{x:e.ml.x+s,y:e.ml.y-o},bl:{x:e.ml.x-s,y:e.ml.y+o},br:{x:e.ml.x+o,y:e.ml.y+s}},e.mt.corner={tl:{x:e.mt.x-o,y:e.mt.y-s},tr:{x:e.mt.x+s,y:e.mt.y-o},bl:{x:e.mt.x-s,y:e.mt.y+o},br:{x:e.mt.x+o,y:e.mt.y+s}},e.mr.corner={tl:{x:e.mr.x-o,y:e.mr.y-s},tr:{x:e.mr.x+s,y:e.mr.y-o},bl:{x:e.mr.x-s,y:e.mr.y+o},br:{x:e.mr.x+o,y:e.mr.y+s}},e.mb.corner={tl:{x:e.mb.x-o,y:e.mb.y-s},tr:{x:e.mb.x+s,y:e.mb.y-o},bl:{x:e.mb.x-s,y:e.mb.y+o},br:{x:e.mb.x+o,y:e.mb.y+s}},e.mtr.corner={tl:{x:e.mtr.x-o+u*this.rotatingPointOffset,y:e.mtr.y-s-a*this.rotatingPointOffset},tr:{x:e.mtr.x+s+u*this.rotatingPointOffset,y:e.mtr.y-o-a*this.rotatingPointOffset},bl:{x:e.mtr.x-s+u*this.rotatingPointOffset,y:e.mtr.y+o-a*this.rotatingPointOffset},br:{x:e.mtr.x+o+u*this.rotatingPointOffset,y:e.mtr.y+s-a*this.rotatingPointOffset}}},drawBorders:function(e){if(!this.hasBorders)return this;var t=this.padding,n=t*2,r=~~(this.strokeWidth/2)*2;e.save(),e.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,e.strokeStyle=this.borderColor;var i=1/this._constrainScale(this.scaleX),s=1/this._constrainScale(this.scaleY);e.lineWidth=1/this.borderScaleFactor,e.scale(i,s);var o=this.getWidth(),u=this.getHeight();e.strokeRect(~~(-(o/2)-t-r/2*this.scaleX)-.5,~~(-(u/2)-t-r/2*this.scaleY)-.5,~~(o+n+r*this.scaleX)+1,~~(u+n+r*this.scaleY)+1);if(this.hasRotatingPoint&&this.isControlVisible("mtr")&&!this.get("lockRotation")&&this.hasControls){var a=(this.flipY?u+r*this.scaleY+t*2:-u-r*this.scaleY-t*2)/2;e.beginPath(),e.moveTo(0,a),e.lineTo(0,a+(this.flipY?this.rotatingPointOffset:-this.rotatingPointOffset)),e.closePath(),e.stroke()}return e.restore(),this},drawControls:function(e){if(!this.hasControls)return this;var t=this.cornerSize,n=t/2,r=~~(this.strokeWidth/2),i=-(this.width/2),s=-(this.height/2),o=this.padding/this.scaleX,u=this.padding/this.scaleY,a=n/this.scaleY,f=n/this.scaleX,l=(n-t)/this.scaleX,c=(n-t)/this.scaleY,h=this.height,p=this.width,d=this.transparentCorners?"strokeRect":"fillRect";return e.save(),e.lineWidth=1/Math.max(this.scaleX,this.scaleY),e.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,e.strokeStyle=e.fillStyle=this.cornerColor,this._drawControl("tl",e,d,i-f-r-o,s-a-r-u),this._drawControl("tr",e,d,i+p-f+r+o,s-a-r-u),this._drawControl("tr",e,d,i-f-r-o,s+h+c+r+u),this._drawControl("br",e,d,i+p+l+r+o,s+h+c+r+u),this.get("lockUniScaling")||(this._drawControl("mt",e,d,i+p/2-f,s-a-r-u),this._drawControl("mb",e,d,i+p/2-f,s+h+c+r+u),this._drawControl("mb",e,d,i+p+l+r+o,s+h/2-a),this._drawControl("ml",e,d,i-f-r-o,s+h/2-a)),this.hasRotatingPoint&&this._drawControl("mtr",e,d,i+p/2-f,this.flipY?s+h+this.rotatingPointOffset/this.scaleY-this.cornerSize/this.scaleX/2+r+u:s-this.rotatingPointOffset/this.scaleY-this.cornerSize/this.scaleY/2-r-u),e.restore(),this},_drawControl:function(e,t,r,i,s){var o=this.cornerSize/this.scaleX,u=this.cornerSize/this.scaleY;this.isControlVisible(e)&&(n||this.transparentCorners||t.clearRect(i,s,o,u),t[r](i,s,o,u))},isControlVisible:function(e){return this._getControlsVisibility()[e]},setControlVisible:function(e,t){return this._getControlsVisibility()[e]=t,this},setControlsVisibility:function(e){e||(e={});for(var t in e)this.setControlVisible(t,e[t]);return this},_getControlsVisibility:function(){return this._controlsVisibility||(this._controlsVisibility={tl:!0,tr:!0,br:!0,bl:!0,ml:!0,mt:!0,mr:!0,mb:!0,mtr:!0}),this._controlsVisibility}})}(),fabric.util.object.extend(fabric.StaticCanvas.prototype,{FX_DURATION:500,fxCenterObjectH:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("left"),endValue:this.getCenter().left,duration:this.FX_DURATION,onChange:function(t){e.set("left",t),s.renderAll(),i()},onComplete:function(){e.setCoords(),r()}}),this},fxCenterObjectV:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("top"),endValue:this.getCenter().top,duration:this.FX_DURATION,onChange:function(t){e.set("top",t),s.renderAll(),i()},onComplete:function(){e.setCoords(),r()}}),this},fxRemove:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("opacity"),endValue:0,duration:this.FX_DURATION,onStart:function(){e.set("active",!1)},onChange:function(t){e.set("opacity",t),s.renderAll(),i()},onComplete:function(){s.remove(e),r()}}),this}}),fabric.util.object.extend(fabric.Object.prototype,{animate:function(){if(arguments[0]&&typeof arguments[0]=="object"){var e=[],t,n;for(t in arguments[0])e.push(t);for(var r=0,i=e.length;r'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Line.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),t.Line.fromElement=function(e,r){var i=t.parseAttributes(e,t.Line.ATTRIBUTE_NAMES),s=[i.x1||0,i.y1||0,i.x2||0,i.y2||0];return new t.Line(s,n(i,r))},t.Line.fromObject=function(e){var n=[e.x1,e.y1,e.x2,e.y2];return new t.Line(n,e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function i(e){return"radius"in e&&e.radius>0}var t=e.fabric||(e.fabric={}),n=Math.PI*2,r=t.util.object.extend;if(t.Circle){t.warn("fabric.Circle is already defined.");return}t.Circle=t.util.createClass(t.Object,{type:"circle",initialize:function(e){e=e||{},this.set("radius",e.radius||0),this.callSuper("initialize",e)},_set:function(e,t){return this.callSuper("_set",e,t),e==="radius"&&this.setRadius(t),this},toObject:function(e){return r(this.callSuper("toObject",e),{radius:this.get("radius")})},toSVG:function(e){var t=this._createBaseSVGMarkup();return t.push("'),e?e(t.join("")):t.join("")},_render:function(e,t){e.beginPath(),e.globalAlpha=this.group?e.globalAlpha*this.opacity:this.opacity,e.arc(t?this.left:0,t?this.top:0,this.radius,0,n,!1),e.closePath(),this._renderFill(e),this._renderStroke(e)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(e){this.radius=e,this.set("width",e*2).set("height",e*2)},complexity:function(){return 1}}),t.Circle.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),t.Circle.fromElement=function(e,n){n||(n={});var s=t.parseAttributes(e,t.Circle.ATTRIBUTE_NAMES);if(!i(s))throw new Error("value of `r` attribute is required and can not be negative");"left"in s&&(s.left-=n.width/2||0),"top"in s&&(s.top-=n.height/2||0);var o=new t.Circle(r(s,n));return o.cx=parseFloat(e.getAttribute("cx"))||0,o.cy=parseFloat(e.getAttribute("cy"))||0,o},t.Circle.fromObject=function(e){return new t.Circle(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Triangle){t.warn("fabric.Triangle is already defined");return}t.Triangle=t.util.createClass(t.Object,{type:"triangle",initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("width",e.width||100).set("height",e.height||100)},_render:function(e){var t=this.width/2,n=this.height/2;e.beginPath(),e.moveTo(-t,n),e.lineTo(0,-n),e.lineTo(t,n),e.closePath(),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n=this.width/2,r=this.height/2;e.beginPath(),t.util.drawDashedLine(e,-n,r,0,-r,this.strokeDashArray),t.util.drawDashedLine(e,0,-r,n,r,this.strokeDashArray),t.util.drawDashedLine(e,n,r,-n,r,this.strokeDashArray),e.closePath()},toSVG:function(e){var t=this._createBaseSVGMarkup(),n=this.width/2,r=this.height/2,i=[-n+" "+r,"0 "+ -r,n+" "+r].join(",");return t.push("'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Triangle.fromObject=function(e){return new t.Triangle(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=Math.PI*2,r=t.util.object.extend;if(t.Ellipse){t.warn("fabric.Ellipse is already defined.");return}t.Ellipse=t.util.createClass(t.Object,{type:"ellipse",rx:0,ry:0,initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("rx",e.rx||0),this.set("ry",e.ry||0),this.set("width",this.get("rx")*2),this.set("height",this.get("ry")*2)},toObject:function(e){return r(this.callSuper("toObject",e),{rx:this.get("rx"),ry:this.get("ry")})},toSVG:function(e){var t=this._createBaseSVGMarkup();return t.push("'),e?e(t.join("")):t.join("")},render:function(e,t){if(this.rx===0||this.ry===0)return;return this.callSuper("render",e,t)},_render:function(e,t){e.beginPath(),e.save(),e.globalAlpha=this.group?e.globalAlpha*this.opacity:this.opacity,this.transformMatrix&&this.group&&e.translate(this.cx,this.cy),e.transform(1,0,0,this.ry/this.rx,0,0),e.arc(t?this.left:0,t?this.top:0,this.rx,0,n,!1),this._renderFill(e),this._renderStroke(e),e.restore()},complexity:function(){return 1}}),t.Ellipse.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),t.Ellipse.fromElement=function(e,n){n||(n={});var i=t.parseAttributes(e,t.Ellipse.ATTRIBUTE_NAMES),s=i.left,o=i.top;"left"in i&&(i.left-=n.width/2||0),"top"in i&&(i.top-=n.height/2||0);var u=new t.Ellipse(r(i,n));return u.cx=s||0,u.cy=o||0,u},t.Ellipse.fromObject=function(e){return new t.Ellipse(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function i(e){return e.left=e.left||0,e.top=e.top||0,e}var t=e.fabric||(e.fabric={}),n=t.util.object.extend;if(t.Rect){console.warn("fabric.Rect is already defined");return}var r=t.Object.prototype.stateProperties.concat();r.push("rx","ry","x","y"),t.Rect=t.util.createClass(t.Object,{stateProperties:r,type:"rect",rx:0,ry:0,x:0,y:0,strokeDashArray:null,initialize:function(e){e=e||{},this.callSuper("initialize",e),this._initRxRy(),this.x=e.x||0,this.y=e.y||0},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(e){var t=this.rx||0,n=this.ry||0,r=-this.width/2,i=-this.height/2,s=this.width,o=this.height,u=this.group&&this.group.type==="path-group";e.beginPath(),e.globalAlpha=u?e.globalAlpha*this.opacity:this.opacity,this.transformMatrix&&u&&e.translate(this.width/2+this.x,this.height/2+this.y),!this.transformMatrix&&u&&e.translate(-this.group.width/2+this.width/2+this.x,-this.group.height/2+this.height/2+this.y);var a=t!==0||n!==0;e.moveTo(r+t,i),e.lineTo(r+s-t,i),a&&e.quadraticCurveTo(r+s,i,r+s,i+n,r+s,i+n),e.lineTo(r+s,i+o-n),a&&e.quadraticCurveTo(r+s,i+o,r+s-t,i+o,r+s-t,i+o),e.lineTo(r+t,i+o),a&&e.quadraticCurveTo(r,i+o,r,i+o-n,r,i+o-n),e.lineTo(r,i+n),a&&e.quadraticCurveTo(r,i,r+t,i,r+t,i),e.closePath(),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n=-this.width/2,r=-this.height/2,i=this.width,s=this.height;e.beginPath(),t.util.drawDashedLine(e,n,r,n+i,r,this.strokeDashArray),t.util.drawDashedLine(e,n+i,r,n+i,r+s,this.strokeDashArray),t.util.drawDashedLine(e,n+i,r+s,n,r+s,this.strokeDashArray),t.util.drawDashedLine(e,n,r+s,n,r,this.strokeDashArray),e.closePath()},_normalizeLeftTopProperties:function(e){return"left"in e&&this.set("left",e.left+this.getWidth()/2),this.set("x",e.left||0),"top"in e&&this.set("top",e.top+this.getHeight()/2),this.set("y",e.top||0),this},toObject:function(e){var t=n(this.callSuper("toObject",e),{rx:this.get("rx")||0,ry:this.get("ry")||0,x:this.get("x"),y:this.get("y")});return this.includeDefaultValues||this._removeDefaultValues(t),t},toSVG:function(e){var t=this._createBaseSVGMarkup();return t.push("'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Rect.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),t.Rect.fromElement=function(e,r){if(!e)return null;var s=t.parseAttributes(e,t.Rect.ATTRIBUTE_NAMES);s=i(s);var o=new t.Rect(n(r?t.util.object.clone(r):{},s));return o._normalizeLeftTopProperties(s),o},t.Rect.fromObject=function(e){return new t.Rect(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.toFixed;if(t.Polyline){t.warn("fabric.Polyline is already defined");return}t.Polyline=t.util.createClass(t.Object,{type:"polyline",initialize:function(e,t,n){t=t||{},this.set("points",e),this.callSuper("initialize",t),this._calcDimensions(n)},_calcDimensions:function(e){return t.Polygon.prototype._calcDimensions.call(this,e)},toObject:function(e){return t.Polygon.prototype.toObject.call(this,e)},toSVG:function(e){var t=[],r=this._createBaseSVGMarkup();for(var i=0,s=this.points.length;i'),e?e(r.join("")):r.join("")},_render:function(e){var t;e.beginPath(),e.moveTo(this.points[0].x,this.points[0].y);for(var n=0,r=this.points.length;n'),e?e(n.join("")):n.join("")},_render:function(e){var t;e.beginPath(),e.moveTo(this.points[0].x,this.points[0].y);for(var n=0,r=this.points.length;n"},toObject:function(e){var t=s(this.callSuper("toObject",e),{path:this.path,pathOffset:this.pathOffset});return this.sourcePath&&(t.sourcePath=this.sourcePath),this.transformMatrix&&(t.transformMatrix=this.transformMatrix),t},toDatalessObject:function(e){var t=this.toObject(e);return this.sourcePath&&(t.path=this.sourcePath),delete t.sourcePath,t},toSVG:function(e){var t=[],n=this._createBaseSVGMarkup();for(var r=0,i=this.path.length;r',"",""),e?e(n.join("")):n.join("")},complexity:function(){return this.path.length},_parsePath:function(){var e=[],n=[],r,i,s=/(-?\.\d+)|(-?\d+(\.\d+)?)/g,o,u;for(var a=0,f,l=this.path.length;ad)for(var v=1,m=f.length;v"];for(var r=0,i=t.length;r"),e?e(n.join("")):n.join("")},toString:function(){return"#"},isSameColor:function(){var e=this.getObjects()[0].get("fill");return this.getObjects().every(function(t){return t.get("fill")===e})},complexity:function(){return this.paths.reduce(function(e,t){return e+(t&&t.complexity?t.complexity():0)},0)},getObjects:function(){return this.paths}}),t.PathGroup.fromObject=function(e,n){typeof e.paths=="string"?t.loadSVGFromURL(e.paths,function(r){var i=e.paths;delete e.paths;var s=t.util.groupSVGElements(r,e,i);n(s)}):t.util.enlivenObjects(e.paths,function(r){delete e.paths,n(new t.PathGroup(r,e))})},t.PathGroup.async=!0}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.array.min,i=t.util.array.max,s=t.util.array.invoke;if(t.Group)return;var o={lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0};t.Group=t.util.createClass(t.Object,t.Collection,{type:"group",initialize:function(e,t){t=t||{},this._objects=e||[];for(var r=this._objects.length;r--;)this._objects[r].group=this;this.originalState={},this.callSuper("initialize"),this._calcBounds(),this._updateObjectsCoords(),t&&n(this,t),this._setOpacityIfSame(),this.setCoords(!0),this.saveCoords()},_updateObjectsCoords:function(){this.forEachObject(this._updateObjectCoords,this)},_updateObjectCoords:function(e){var t=e.getLeft(),n=e.getTop();e.set({originalLeft:t,originalTop:n,left:t-this.left,top:n-this.top}),e.setCoords(),e.__origHasControls=e.hasControls,e.hasControls=!1},toString:function(){return"#"},addWithUpdate:function(e){return this._restoreObjectsState(),this._objects.push(e),e.group=this,this.forEachObject(this._setObjectActive,this),this._calcBounds(),this._updateObjectsCoords(),this},_setObjectActive:function(e){e.set("active",!0),e.group=this},removeWithUpdate:function(e){return this._moveFlippedObject(e),this._restoreObjectsState(),this.forEachObject(this._setObjectActive,this),this.remove(e),this._calcBounds(),this._updateObjectsCoords(),this},_onObjectAdded:function(e){e.group=this},_onObjectRemoved:function(e){delete e.group,e.set("active",!1)},delegatedProperties:{fill:!0,opacity:!0,fontFamily:!0,fontWeight:!0,fontSize:!0,fontStyle:!0,lineHeight:!0,textDecoration:!0,textAlign:!0,backgroundColor:!0},_set:function(e,t){if(e in this.delegatedProperties){var n=this._objects.length;this[e]=t;while(n--)this._objects[n].set(e,t)}else this[e]=t},toObject:function(e){return n(this.callSuper("toObject",e),{objects:s(this._objects,"toObject",e)})},render:function(e,n){if(!this.visible)return;e.save(),this.transform(e),this.clipTo&&t.util.clipContext(this,e);for(var r=0,i=this._objects.length;r'];for(var n=0,r=this._objects.length;n"),e?e(t.join("")):t.join("")},get:function(e){if(e in o){if(this[e])return this[e];for(var t=0,n=this._objects.length;t','");if(this.stroke||this.strokeDashArray){var n=this.fill;this.fill=null,t.push("'),this.fill=n}return t.push(""),e?e(t.join("")):t.join("")},getSrc:function(){return this.getElement().src||this.getElement()._src},toString:function(){return'#'},clone:function(e,t){this.constructor.fromObject(this.toObject(t),e)},applyFilters:function(e){if(this.filters.length===0){this._element=this._originalElement,e&&e();return}var t=this._originalElement,n=fabric.util.createCanvasElement(),r=fabric.util.createImage(),i=this;return n.width=t.width,n.height=t.height,n.getContext("2d").drawImage(t,0,0,t.width,t.height),this.filters.forEach(function(e){e&&e.applyTo(n)}),r.width=t.width,r.height=t.height,fabric.isLikelyNode?(r.src=n.toBuffer(undefined,fabric.Image.pngCompression),i._element=r,e&&e()):(r.onload=function(){i._element=r,e&&e(),r.onload=n=t=null},r.src=n.toDataURL("image/png")),this},_render:function(e){e.drawImage(this._element,-this.width/2,-this.height/2,this.width,this.height)},_resetWidthHeight:function(){var e=this.getElement();this.set("width",e.width),this.set("height",e.height)},_initElement:function(e){this.setElement(fabric.util.getById(e)),fabric.util.addClass(this.getElement(),fabric.Image.CSS_CANVAS)},_initConfig:function(e){e||(e={}),this.setOptions(e),this._setWidthHeight(e),this._element.crossOrigin=this.crossOrigin},_initFilters:function(e,t){e.filters&&e.filters.length?fabric.util.enlivenObjects(e.filters,function(e){t&&t(e)},"fabric.Image.filters"):t&&t()},_setWidthHeight:function(e){this.width="width"in e?e.width:this.getElement().width||0,this.height="height"in e?e.height:this.getElement().height||0},complexity:function(){return 1}}),fabric.Image.CSS_CANVAS="canvas-img",fabric.Image.prototype.getSvgSrc=fabric.Image.prototype.getSrc,fabric.Image.fromObject=function(e,t){fabric.util.loadImage(e.src,function(n){fabric.Image.prototype._initFilters.call(e,e,function(r){e.filters=r||[];var i=new fabric.Image(n,e);t&&t(i)})},null,e.crossOrigin)},fabric.Image.fromURL=function(e,t,n){fabric.util.loadImage(e,function(e){t(new fabric.Image(e,n))},null,n&&n.crossOrigin)},fabric.Image.ATTRIBUTE_NAMES=fabric.SHARED_ATTRIBUTES.concat("x y width height xlink:href".split(" ")),fabric.Image.fromElement=function(e,n,r){var i=fabric.parseAttributes(e,fabric.Image.ATTRIBUTE_NAMES);fabric.Image.fromURL(i["xlink:href"],n,t(r?fabric.util.object.clone(r):{},i))},fabric.Image.async=!0,fabric.Image.pngCompression=1}(typeof exports!="undefined"?exports:this),fabric.util.object.extend(fabric.Object.prototype,{_getAngleValueForStraighten:function(){var e=this.getAngle()%360;return e>0?Math.round((e-1)/90)*90:Math.round(e/90)*90},straighten:function(){return this.setAngle(this._getAngleValueForStraighten()),this},fxStraighten:function(e){e=e||{};var t=function(){},n=e.onComplete||t,r=e.onChange||t,i=this;return fabric.util.animate({startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(e){i.setAngle(e),r()},onComplete:function(){i.setCoords(),n()},onStart:function(){i.set("active",!1)}}),this}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{straightenObject:function(e){return e.straighten(),this.renderAll(),this},fxStraightenObject:function(e){return e.fxStraighten({onChange:this.renderAll.bind(this)}),this}}),fabric.Image.filters=fabric.Image.filters||{},fabric.Image.filters.BaseFilter=fabric.util.createClass({type:"BaseFilter",toObject:function(){return{type:this.type}},toJSON:function(){return this.toObject()}}),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Brightness=t.util.createClass(t.Image.filters.BaseFilter,{type:"Brightness",initialize:function(e){e=e||{},this.brightness=e.brightness||100},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.brightness;for(var s=0,o=r.length;sa||C<0||C>u)continue;var k=(N*u+C)*4,L=t[x*i+T];b+=o[k]*L,w+=o[k+1]*L,E+=o[k+2]*L,S+=o[k+3]*L}h[y]=b,h[y+1]=w,h[y+2]=E,h[y+3]=S+p*(255-S)}n.putImageData(c,0,0)},toObject:function(){return n(this.callSuper("toObject"),{opaque:this.opaque,matrix:this.matrix})}}),t.Image.filters.Convolute.fromObject=function(e){return new t.Image.filters.Convolute(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.GradientTransparency=t.util.createClass(t.Image.filters.BaseFilter,{type:"GradientTransparency",initialize:function(e){e=e||{},this.threshold=e.threshold||100},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.threshold,s=r.length;for(var o=0,u=r.length;o-1?e.channel:0},applyTo:function(e){if(!this.mask)return;var n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=r.data,s=this.mask.getElement(),o=t.util.createCanvasElement(),u=this.channel,a,f=r.width*r.height*4;o.width=s.width,o.height=s.height,o.getContext("2d").drawImage(s,0,0,s.width,s.height);var l=o.getContext("2d").getImageData(0,0,s.width,s.height),c=l.data;for(a=0;ao&&f>o&&l>o&&u(a-f)'},_render:function(e){var t=this.group&&this.group.type==="path-group";t&&!this.transformMatrix?e.translate(-this.group.width/2+this.left,-this.group.height/2+this.top):t&&this.transformMatrix&&e.translate(-this.group.width/2,-this.group.height/2),typeof Cufon=="undefined"||this.useNative===!0?this._renderViaNative(e):this._renderViaCufon(e)},_renderViaNative:function(e){var n=this.text.split(this._reNewline);this.transform(e,t.isLikelyNode),this._setTextStyles(e),this.width=this._getTextWidth(e,n),this.height=this._getTextHeight(e,n),this.clipTo&&t.util.clipContext(this,e),this._renderTextBackground(e,n),this._translateForTextAlign(e),this._renderText(e,n),this.textAlign!=="left"&&this.textAlign!=="justify"&&e.restore(),this._renderTextDecoration(e,n),this.clipTo&&e.restore(),this._setBoundaries(e,n),this._totalLineHeight=0},_renderText:function(e,t){e.save(),this._setShadow(e),this._renderTextFill(e,t),this._renderTextStroke(e,t),this._removeShadow(e),e.restore()},_translateForTextAlign:function(e){this.textAlign!=="left"&&this.textAlign!=="justify"&&(e.save(),e.translate(this.textAlign==="center"?this.width/2:this.width,0))},_setBoundaries:function(e,t){this._boundaries=[];for(var n=0,r=t.length;nn&&(n=s)}return n},_renderChars:function(e,t,n,r,i){t[e](n,r,i)},_renderTextLine:function(e,t,n,r,i,s){i-=this.fontSize/4;if(this.textAlign!=="justify"){this._renderChars(e,t,n,r,i,s);return}var o=t.measureText(n).width,u=this.width;if(u>o){var a=n.split(/\s+/),f=t.measureText(n.replace(/\s+/g,"")).width,l=u-f,c=a.length-1,h=l/c,p=0;for(var d=0,v=a.length;d-1&&i(this.fontSize*this.lineHeight),this.textDecoration.indexOf("line-through")>-1&&i(this.fontSize*this.lineHeight-this.fontSize/2),this.textDecoration.indexOf("overline")>-1&&i(this.fontSize*this.lineHeight-this.fontSize)},_getFontDeclaration:function(){return[t.isLikelyNode?this.fontWeight:this.fontStyle,t.isLikelyNode?this.fontStyle:this.fontWeight,this.fontSize+"px",t.isLikelyNode?'"'+this.fontFamily+'"':this.fontFamily].join(" ")},render:function(e,t){if(!this.visible)return;e.save(),this._render(e),!t&&this.active&&(this.drawBorders(e),this.drawControls(e)),e.restore()},toObject:function(e){var t=n(this.callSuper("toObject",e),{text:this.text,fontSize:this.fontSize,fontWeight:this.fontWeight,fontFamily:this.fontFamily,fontStyle:this.fontStyle,lineHeight:this.lineHeight,textDecoration:this.textDecoration,textAlign:this.textAlign,path:this.path,textBackgroundColor:this.textBackgroundColor,useNative:this.useNative});return this.includeDefaultValues||this._removeDefaultValues(t),t},toSVG:function(e){var t=[],n=this.text.split(this._reNewline),r=this._getSVGLeftTopOffsets(n),i=this._getSVGTextAndBg(r.lineTop,r.textLeft,n),s=this._getSVGShadows(r.lineTop,n);return r.textTop+=this._fontAscent?this._fontAscent/5*this.lineHeight:0,this._wrapSVGTextAndBg(t,i,s,r),e?e(t.join("")):t.join("")},_getSVGLeftTopOffsets:function(e){var t=this.useNative?this.fontSize*this.lineHeight:-this._fontAscent-this._fontAscent/5*this.lineHeight,n=-(this.width/2),r=this.useNative?this.fontSize-1:this.height/2-e.length*this.fontSize-this._totalLineHeight;return{textLeft:n,textTop:r,lineTop:t}},_wrapSVGTextAndBg:function(e,t,n,r){e.push('',t.textBgRects.join(""),"',n.join(""),t.textSpans.join(""),"","")},_getSVGShadows:function(e,n){var r=[],s,o,u=1;if(!this.shadow||!this._boundaries)return r;for(s=0,o=n.length;s",t.util.string.escapeXml(n[s]),""),u=1}else u++;return r},_getSVGTextAndBg:function(e,t,n){var r=[],i=[],s=1;this._setSVGBg(i);for(var o=0,u=n.length;o",t.util.string.escapeXml(e),"")},_setSVGTextLineBg:function(e,t,n,r){e.push("')},_setSVGBg:function(e){this.backgroundColor&&this._boundaries&&e.push("')},_getFillAttributes:function(e){var n=e&&typeof e=="string"?new t.Color(e):"";return!n||!n.getSource()||n.getAlpha()===1?'fill="'+e+'"':'opacity="'+n.getAlpha()+'" fill="'+n.setAlpha(1).toRgb()+'"'},_set:function(e,t){e==="fontFamily"&&this.path&&(this.path=this.path.replace(/(.*?)([^\/]*)(\.font\.js)/,"$1"+t+"$3")),this.callSuper("_set",e,t),e in this._dimensionAffectingProps&&(this._initDimensions(),this.setCoords())},complexity:function(){return 1}}),t.Text.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x y font-family font-style font-weight font-size text-decoration".split(" ")),t.Text.fromElement=function(e,n){if(!e)return null;var r=t.parseAttributes(e,t.Text.ATTRIBUTE_NAMES);n=t.util.object.extend(n?t.util.object.clone(n):{},r);var i=new t.Text(e.textContent,n);return i.set({left:i.getLeft()+i.getWidth()/2,top:i.getTop()-i.getHeight()/2}),i},t.Text.fromObject=function(e){return new t.Text(e.text,r(e))},t.util.createAccessors(t.Text)}(typeof exports!="undefined"?exports:this),function(){var e=fabric.util.object.clone;fabric.IText=fabric.util.createClass(fabric.Text,fabric.Observable,{type:"i-text",selectionStart:0,selectionEnd:0,selectionColor:"rgba(17,119,255,0.3)",isEditing:!1,editable:!0,editingBorderColor:"rgba(102,153,255,0.25)",cursorWidth:2,cursorColor:"#333",cursorDelay:1e3,cursorDuration:600,styles:null,skipFillStrokeCheck:!0,_reSpace:/\s|\n/,_fontSizeFraction:4,_currentCursorOpacity:0,_selectionDirection:null,_abortCursorAnimation:!1,_charWidthsCache:{},initialize:function(e,t){this.styles=t?t.styles||{}:{},this.callSuper("initialize",e,t),this.initBehavior(),fabric.IText.instances.push(this),this.__lineWidths={},this.__lineHeights={},this.__lineOffsets={}},isEmptyStyles:function(){if(!this.styles)return!0;var e=this.styles;for(var t in e)for(var n in e[t])for(var r in e[t][n])return!1;return!0},setSelectionStart:function(e){this.selectionStart=e,this.hiddenTextarea&&(this.hiddenTextarea.selectionStart=e)},setSelectionEnd:function(e){this.selectionEnd=e,this.hiddenTextarea&&(this.hiddenTextarea.selectionEnd=e)},getSelectionStyles:function(){var e=this.get2DCursorLocation();return this.styles[e.lineIndex]?this.styles[e.lineIndex][e.charIndex]||{}:{}},setSelectionStyles:function(e){if(this.selectionStart===this.selectionEnd)this._extendStyles(this.selectionStart,e);else for(var t=this.selectionStart;t-1&&this._renderCharDecorationAtOffset(e,n,r+this.fontSize/this._fontSizeFraction,i,0),o.indexOf("line-through")>-1&&this._renderCharDecorationAtOffset(e,n,r+this.fontSize/this._fontSizeFraction,i,s/this._fontSizeFraction),o.indexOf("overline")>-1&&this._renderCharDecorationAtOffset(e,n,r,i,s-this.fontSize/this._fontSizeFraction)},_renderCharDecorationAtOffset:function(e,t,n,r,i){e.fillRect(t,n-i,r,1)},_renderTextLine:function(e,t,n,r,i,s){i+=this.fontSize/4,this.callSuper("_renderTextLine",e,t,n,r,i,s)},_renderTextDecoration:function(e,t){if(this.isEmptyStyles())return this.callSuper("_renderTextDecoration",e,t)},_renderTextLinesBackground:function(e,t){if(!this.textBackgroundColor&&!this.styles)return;e.save(),this.textBackgroundColor&&(e.fillStyle=this.textBackgroundColor);var n=0,r=this.fontSize/this._fontSizeFraction;for(var i=0,s=t.length;in&&(n=s)}return n},_getHeightOfLine:function(e,t,n){n=n||this.text.split(this._reNewline);var r=this._getHeightOfChar(e,n[t][0],t,0),i=n[t],s=i.split("");for(var o=1,u=s.length;or&&(r=a)}return r*this.lineHeight},_getTextHeight:function(e,t){var n=0;for(var r=0,i=t.length;r-1)t++,n--;return e-t},findWordBoundaryRight:function(e){var t=0,n=e;if(this._reSpace.test(this.text.charAt(n)))while(this._reSpace.test(this.text.charAt(n)))t++,n++;while(/\S/.test(this.text.charAt(n))&&n-1)t++,n--;return e-t},findLineBoundaryRight:function(e){var t=0,n=e;while(!/\n/.test(this.text.charAt(n))&&n0&&nr;s?this.removeStyleObject(s,n+1):this.removeStyleObject(this.get2DCursorLocation(n).charIndex===0,n)}this.text=this.text.slice(0,e)+this.text.slice(t)},insertChars:function(e){var t=this.text.slice(this.selectionStart,this.selectionStart+1)==="\n";this.text=this.text.slice(0,this.selectionStart)+e+this.text.slice(this.selectionEnd),this.selectionStart===this.selectionEnd?this.insertStyleObject(e,t):this.selectionEnd-this.selectionStart>1,this.selectionStart+=e.length,this.selectionEnd=this.selectionStart,this.canvas&&this.canvas.renderAll().renderAll(),this.setCoords(),this.fire("text:changed")},insertNewlineStyleObject:function(t,n,r){this.shiftLineStyles(t,1),this.styles[t+1]||(this.styles[t+1]={});var i=this.styles[t][n-1],s={};if(r)s[0]=e(i),this.styles[t+1]=s;else{for(var o in this.styles[t])parseInt(o,10)>=n&&(s[parseInt(o,10)-n]=this.styles[t][o],delete this.styles[t][o]);this.styles[t+1]=s}},insertCharStyleObject:function(t,n){var r=this.styles[t],i=e(r);n===0&&(n=1);for(var s in i){var o=parseInt(s,10);o>=n&&(r[o+1]=i[o])}this.styles[t][n]=e(r[n-1])},insertStyleObject:function(e,t){if(this.isEmptyStyles())return;var n=this.get2DCursorLocation(),r=n.lineIndex,i=n.charIndex;this.styles[r]||(this.styles[r]={}),e==="\n"?this.insertNewlineStyleObject(r,i,t):this.insertCharStyleObject(r,i)},shiftLineStyles:function(t,n){var r=e(this.styles);for(var i in this.styles){var s=parseInt(i,10);s>t&&(this.styles[s+n]=r[s])}},removeStyleObject:function(t,n){var r=this.get2DCursorLocation(n),i=r.lineIndex,s=r.charIndex;if(t){var o=this.text.split(this._reNewline),u=o[i-1],a=u.length;this.styles[i-1]||(this.styles[i-1]={});for(s in this.styles[i])this.styles[i-1][parseInt(s,10)+a]=this.styles[i][s];this.shiftLineStyles(i,-1)}else{var f=this.styles[i];if(f){var l=this.selectionStart===this.selectionEnd?-1:0;delete f[s+l]}var c=e(f);for(var h in c){var p=parseInt(h,10);p>=s&&p!==0&&(f[p-1]=c[p],delete f[p])}}},insertNewline:function(){this.insertChars("\n")}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+(new Date),this.__lastLastClickTime=+(new Date),this.lastPointer={},this.on("mousedown",this.onMouseDown.bind(this))},onMouseDown:function(e){this.__newClickTime=+(new Date);var t=this.canvas.getPointer(e.e);this.isTripleClick(t)?(this.fire("tripleclick",e),this._stopEvent(e.e)):this.isDoubleClick(t)&&(this.fire("dblclick",e),this._stopEvent(e.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=t},isDoubleClick:function(e){return this.__newClickTime-this.__lastClickTime<500&&this.__lastPointer.x===e.x&&this.__lastPointer.y===e.y},isTripleClick:function(e){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===e.x&&this.__lastPointer.y===e.y},_stopEvent:function(e){e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation()},initCursorSelectionHandlers:function(){this.initSelectedHandler(),this.initMousedownHandler(),this.initMousemoveHandler(),this.initMouseupHandler(),this.initClicks()},initClicks:function(){this.on("dblclick",function(e){this.selectWord(this.getSelectionStartFromPointer(e.e))}),this.on("tripleclick",function(e){this.selectLine(this.getSelectionStartFromPointer(e.e))})},initMousedownHandler:function(){this.on("mousedown",function(e){var t=this.canvas.getPointer(e.e);this.__mousedownX=t.x,this.__mousedownY=t.y,this.__isMousedown=!0,this.hiddenTextarea&&this.canvas&&this.canvas.wrapperEl.appendChild(this.hiddenTextarea),this.isEditing?(this.setCursorByClick(e.e),this.__selectionStartOnMouseDown=this.selectionStart):this.exitEditingOnOthers()})},initMousemoveHandler:function(){this.on("mousemove",function(e){if(!this.__isMousedown||!this.isEditing)return;var t=this.getSelectionStartFromPointer(e.e);t>=this.__selectionStartOnMouseDown?(this.setSelectionStart(this.__selectionStartOnMouseDown),this.setSelectionEnd(t)):(this.setSelectionStart(t),this.setSelectionEnd(this.__selectionStartOnMouseDown))})},_isObjectMoved:function(e){var t=this.canvas.getPointer(e);return this.__mousedownX!==t.x||this.__mousedownY!==t.y},initMouseupHandler:function(){this.on("mouseup",function(e){this.__isMousedown=!1;if(this._isObjectMoved(e.e))return;this.selected&&this.enterEditing()})},setCursorByClick:function(e){var t=this.getSelectionStartFromPointer(e);e.shiftKey?ts?0:1,a=r+u;return this.flipX&&(a=i-a),a>this.text.length&&(a=this.text.length),a}}),fabric.util.object.extend(fabric.IText.prototype,{initKeyHandlers:function(){fabric.util.addListener(fabric.document,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(fabric.document,"keypress",this.onKeyPress.bind(this))},initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.style.cssText="position: absolute; top: 0; left: -9999px",fabric.document.body.appendChild(this.hiddenTextarea)},_keysMap:{8:"removeChars",13:"insertNewline",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown",46:"forwardDelete"},_ctrlKeysMap:{65:"selectAll",67:"copy",86:"paste",88:"cut"},onKeyDown:function(e){if(!this.isEditing)return;if(e.keyCode in this._keysMap)this[this._keysMap[e.keyCode]](e);else{if(!(e.keyCode in this._ctrlKeysMap&&(e.ctrlKey||e.metaKey)))return;this[this._ctrlKeysMap[e.keyCode]](e)}e.preventDefault(),e.stopPropagation(),this.canvas&&this.canvas.renderAll()},forwardDelete:function(e){this.selectionStart===this.selectionEnd&&this.moveCursorRight(e),this.removeChars(e)},copy:function(){var e=this.getSelectedText();this.copiedText=e},paste:function(){this.copiedText&&this.insertChars(this.copiedText)},cut:function(e){this.copy(),this.removeChars(e)},onKeyPress:function(e){if(!this.isEditing||e.metaKey||e.ctrlKey||e.keyCode===8||e.keyCode===13)return;this.insertChars(String.fromCharCode(e.which)),e.preventDefault(),e.stopPropagation()},getDownCursorOffset:function(e,t){var n=t?this.selectionEnd:this.selectionStart,r=this.text.split(this._reNewline),i,s,o=this.text.slice(0,n),u=this.text.slice(n),a=o.slice(o.lastIndexOf("\n")+1),f=u.match(/(.*)\n?/)[1],l=(u.match(/.*\n(.*)\n?/)||{})[1]||"",c=this.get2DCursorLocation(n);if(c.lineIndex===r.length-1||e.metaKey)return this.text.length-n;var h=this._getWidthOfLine(this.ctx,c.lineIndex,r);s=this._getLineLeftOffset(h);var p=s,d=c.lineIndex;for(var v=0,m=a.length;vn){f=!0;var d=u-p,v=u,m=Math.abs(d-n),g=Math.abs(v-n);a=gthis.text.length&&(this.selectionStart=this.text.length),this.selectionEnd=this.selectionStart},moveCursorDownWithShift:function(e){if(this._selectionDirection==="left"&&this.selectionStart!==this.selectionEnd){this.selectionStart+=e,this._selectionDirection="left";return}this._selectionDirection="right",this.selectionEnd+=e,this.selectionEnd>this.text.length&&(this.selectionEnd=this.text.length)},getUpCursorOffset:function(e,t){var n=t?this.selectionEnd:this.selectionStart,r=this.get2DCursorLocation(n);if(r.lineIndex===0||e.metaKey)return n;var i=this.text.slice(0,n),s=i.slice(i.lastIndexOf("\n")+1),o=(i.match(/\n?(.*)\n.*$/)||{})[1]||"",u=this.text.split(this._reNewline),a,f,l=this._getWidthOfLine(this.ctx,r.lineIndex,u);f=this._getLineLeftOffset(l);var c=f,h=r.lineIndex;for(var p=0,d=s.length;pn){f=!0;var d=u-p,v=u,m=Math.abs(d-n),g=Math.abs(v-n);a=g=this.text.length&&this.selectionEnd>=this.text.length)return;this.abortCursorAnimation(),this._currentCursorOpacity=1,e.shiftKey?this.moveCursorRightWithShift(e):this.moveCursorRightWithoutShift(e),this.initDelayedCursor()},moveCursorRightWithShift:function(e){this._selectionDirection==="left"&&this.selectionStart!==this.selectionEnd?this._moveRight(e,"selectionStart"):(this._selectionDirection="right",this._moveRight(e,"selectionEnd"),this.text.charAt(this.selectionEnd-1)==="\n"&&this.selectionEnd++,this.selectionEnd>this.text.length&&(this.selectionEnd=this.text.length))},moveCursorRightWithoutShift:function(e){this._selectionDirection="right",this.selectionStart===this.selectionEnd?(this._moveRight(e,"selectionStart"),this.selectionEnd=this.selectionStart):(this.selectionEnd+=this.getNumNewLinesInSelectedText(),this.selectionEnd>this.text.length&&(this.selectionEnd=this.text.length),this.selectionStart=this.selectionEnd)},removeChars:function(e){this.selectionStart===this.selectionEnd?this._removeCharsNearCursor(e):this._removeCharsFromTo(this.selectionStart,this.selectionEnd),this.selectionEnd=this.selectionStart,this._removeExtraneousStyles(),this.canvas&&this.canvas.renderAll().renderAll(),this.setCoords(),this.fire("text:changed")},_removeCharsNearCursor:function(e){if(this.selectionStart!==0)if(e.metaKey){var t=this.findLineBoundaryLeft(this.selectionStart);this._removeCharsFromTo(t,this.selectionStart),this.selectionStart=t}else if(e.altKey){var n=this.findWordBoundaryLeft(this.selectionStart);this._removeCharsFromTo(n,this.selectionStart),this.selectionStart=n}else{var r=this.text.slice(this.selectionStart-1,this.selectionStart)==="\n";this.removeStyleObject(r),this.selectionStart--,this.text=this.text.slice(0,this.selectionStart)+this.text.slice(this.selectionStart+1)}}}),fabric.util.object.extend(fabric.IText.prototype,{_setSVGTextLineText:function(e,t,n,r,i,s){this.styles[t]?this._setSVGTextLineChars(e,t,n,r,i,s):this.callSuper("_setSVGTextLineText",e,t,n,r,i)},_setSVGTextLineChars:function(e,t,n,r,i,s){var o=t===0||this.useNative?"y":"dy",u=e.split(""),a=0,f=this._getSVGLineLeftOffset(t),l=this._getSVGLineTopOffset(t),c=this._getHeightOfLine(this.ctx,t);for(var h=0,p=u.length;h'].join("")},_createTextCharSpan:function(e,t,n,r,i,s){var o=this.getSvgStyles.call(fabric.util.object.extend({visible:!0,fill:this.fill,stroke:this.stroke,type:"text"},t));return['',fabric.util.string.escapeXml(e),""].join("")}}),function(){function request(e,t,n){var r=URL.parse(e);r.port||(r.port=r.protocol.indexOf("https:")===0?443:80);var i=r.port===443?HTTPS:HTTP,s=i.request({hostname:r.hostname,port:r.port,path:r.path,method:"GET"},function(e){var r="";t&&e.setEncoding(t),e.on("end",function(){n(r)}),e.on("data",function(t){e.statusCode===200&&(r+=t)})});s.on("error",function(e){e.errno===process.ECONNREFUSED?fabric.log("ECONNREFUSED: connection refused to "+r.hostname+":"+r.port):fabric.log(e.message)}),s.end()}function request_fs(e,t){var n=require("fs");n.readFile(e,function(e,n){if(e)throw fabric.log(e),e;t(n)})}if(typeof document!="undefined"&&typeof window!="undefined")return;var DOMParser=(new require("xmldom")).DOMParser,URL=require("url"),HTTP=require("http"),HTTPS=require("https"),Canvas=require("canvas"),Image=require("canvas").Image;fabric.util.loadImage=function(e,t,n){var r=function(r){i.src=new Buffer(r,"binary"),i._src=e,t&&t.call(n,i)},i=new Image;e&&(e instanceof Buffer||e.indexOf("data")===0)?(i.src=i._src=e,t&&t.call(n,i)):e&&e.indexOf("http")!==0?request_fs(e,r):e?request(e,"binary",r):t&&t.call(n,e)},fabric.loadSVGFromURL=function(e,t,n){e=e.replace(/^\n\s*/,"").replace(/\?.*$/,"").trim(),e.indexOf("http")!==0?request_fs(e,function(e){fabric.loadSVGFromString(e,t,n)}):request(e,"",function(e){fabric.loadSVGFromString(e,t,n)})},fabric.loadSVGFromString=function(e,t,n){var r=(new DOMParser).parseFromString(e);fabric.parseSVGDocument(r.documentElement,function(e,n){t&&t(e,n)},n)},fabric.util.getScript=function(url,callback){request(url,"",function(body){eval(body),callback&&callback()})},fabric.Image.fromObject=function(e,t){fabric.util.loadImage(e.src,function(n){var r=new fabric.Image(n);r._initConfig(e),r._initFilters(e,function(e){r.filters=e||[],t&&t(r)})})},fabric.createCanvasForNode=function(e,t){var n=fabric.document.createElement("canvas"),r=new Canvas(e||600,t||600);n.style={},n.width=r.width,n.height=r.height;var i=fabric.Canvas||fabric.StaticCanvas,s=new i(n);return s.contextContainer=r.getContext("2d"),s.nodeCanvas=r,s.Font=Canvas.Font,s},fabric.StaticCanvas.prototype.createPNGStream=function(){return this.nodeCanvas.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(e){return this.nodeCanvas.createJPEGStream(e)};var origSetWidth=fabric.StaticCanvas.prototype.setWidth;fabric.StaticCanvas.prototype.setWidth=function(e){return origSetWidth.call(this,e),this.nodeCanvas.width=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setWidth=fabric.StaticCanvas.prototype.setWidth);var origSetHeight=fabric.StaticCanvas.prototype.setHeight;fabric.StaticCanvas.prototype.setHeight=function(e){return origSetHeight.call(this,e),this.nodeCanvas.height=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setHeight=fabric.StaticCanvas.prototype.setHeight)}(); \ No newline at end of file diff --git a/dist/all.min.js.gz b/dist/all.min.js.gz index 1766762a..c30821d5 100644 Binary files a/dist/all.min.js.gz and b/dist/all.min.js.gz differ diff --git a/dist/all.require.js b/dist/all.require.js index deb5fad2..83f4e68b 100644 --- a/dist/all.require.js +++ b/dist/all.require.js @@ -1,7 +1,11 @@ +<<<<<<< HEAD /* build: `node build.js modules=ALL` */ +======= +/* build: `node build.js modules=ALL exclude=gestures,cufon,json minifier=uglifyjs` */ +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f /*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: "1.3.7" }; +var fabric = fabric || { version: "1.3.12" }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } @@ -12,7 +16,9 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { } else { // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom").jsdom(""); + fabric.document = require("jsdom") + .jsdom(""); + fabric.window = fabric.document.createWindow(); } @@ -26,1729 +32,1489 @@ fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; * True when in environment that's probably Node.js * @type boolean */ -fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; -/*! - * Copyright (c) 2009 Simo Kinnunen. - * Licensed under the MIT license. +/** + * Attributes parsed from all SVG elements + * @type array */ +fabric.SHARED_ATTRIBUTES = [ + "transform", + "fill", "fill-opacity", "fill-rule", + "opacity", + "stroke", "stroke-dasharray", "stroke-linecap", + "stroke-linejoin", "stroke-miterlimit", + "stroke-opacity", "stroke-width" +]; -var Cufon = (function() { - /** @ignore */ - var api = function() { - return api.replace.apply(null, arguments); - }; +(function(){ - /** @ignore */ - var DOM = api.DOM = { + /** + * @private + * @param {String} eventName + * @param {Function} handler + */ + function _removeEventListener(eventName, handler) { + if (!this.__eventListeners[eventName]) return; - ready: (function() { + if (handler) { + fabric.util.removeFromArray(this.__eventListeners[eventName], handler); + } + else { + this.__eventListeners[eventName].length = 0; + } + } - var complete = false, readyStatus = { loaded: 1, complete: 1 }; - - var queue = [], /** @ignore */ perform = function() { - if (complete) return; - complete = true; - for (var fn; fn = queue.shift(); fn()); - }; - - // Gecko, Opera, WebKit r26101+ - - if (fabric.document.addEventListener) { - fabric.document.addEventListener('DOMContentLoaded', perform, false); - fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages + /** + * Observes specified event + * @deprecated `observe` deprecated since 0.8.34 (use `on` instead) + * @memberOf fabric.Observable + * @alias on + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function that receives a notification when an event of the specified type occurs + * @return {Self} thisArg + * @chainable + */ + function observe(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); } - - // Old WebKit, Internet Explorer - - if (!fabric.window.opera && fabric.document.readyState) (function() { - readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); - })(); - - // Internet Explorer - - if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { - try { - fabric.document.body.doScroll('left'); - perform(); - } - catch (e) { - setTimeout(arguments.callee, 1); - } - })(); - - addEvent(fabric.window, 'load', perform); // Fallback - - return function(listener) { - if (!arguments.length) perform(); - else complete ? listener() : queue.push(listener); - }; - - })() - - }; - - /** @ignore */ - var CSS = api.CSS = /** @ignore */ { - - /** @ignore */ - Size: function(value, base) { - - this.value = parseFloat(value); - this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; - - /** @ignore */ - this.convert = function(value) { - return value / base * this.value; - }; - - /** @ignore */ - this.convertFrom = function(value) { - return value / this.value * base; - }; - - /** @ignore */ - this.toString = function() { - return this.value + this.unit; - }; - - }, - - /** @ignore */ - getStyle: function(el) { - return new Style(el.style); - /* - var view = document.defaultView; - if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); - if (el.currentStyle) return new Style(el.currentStyle); - return new Style(el.style); - */ - }, - - quotedList: cached(function(value) { - // doesn't work properly with empty quoted strings (""), but - // it's not worth the extra code. - var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; - while (match = re.exec(value)) list.push(match[3] || match[1]); - return list; - }), - - ready: (function() { - - var complete = false; - - var queue = [], perform = function() { - complete = true; - for (var fn; fn = queue.shift(); fn()); - }; - - // Safari 2 does not include '); + if (arcToSegmentsCache[argsString]) { + return arcToSegmentsCache[argsString]; + } - function getFontSizeInPixels(el, value) { - return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value); - } + var coords = getXYCoords(rotateX, rx, ry, ox, oy, x, y); - // Original by Dead Edwards. - // Combined with getFontSizeInPixels it also works with relative units. - function getSizeInPixels(el, value) { - if (/px$/i.test(value)) return parseFloat(value); - var style = el.style.left, runtimeStyle = el.runtimeStyle.left; - el.runtimeStyle.left = el.currentStyle.left; - el.style.left = value; - var result = el.style.pixelLeft; - el.style.left = style; - el.runtimeStyle.left = runtimeStyle; + var d = (coords.x1-coords.x0) * (coords.x1-coords.x0) + + (coords.y1-coords.y0) * (coords.y1-coords.y0); + + var sfactor_sq = 1 / d - 0.25; + if (sfactor_sq < 0) sfactor_sq = 0; + + var sfactor = Math.sqrt(sfactor_sq); + if (sweep === large) sfactor = -sfactor; + + var xc = 0.5 * (coords.x0 + coords.x1) - sfactor * (coords.y1-coords.y0); + var yc = 0.5 * (coords.y0 + coords.y1) + sfactor * (coords.x1-coords.x0); + + var th0 = Math.atan2(coords.y0-yc, coords.x0-xc); + var th1 = Math.atan2(coords.y1-yc, coords.x1-xc); + + var th_arc = th1-th0; + if (th_arc < 0 && sweep === 1) { + th_arc += 2*Math.PI; + } + else if (th_arc > 0 && sweep === 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i 1) { + pl = Math.sqrt(pl); + rx *= pl; + ry *= pl; } - var wrapper, canvas; + var a00 = cos_th / rx; + var a01 = sin_th / rx; + var a10 = (-sin_th) / ry; + var a11 = (cos_th) / ry; - if (redraw) { - wrapper = node; - canvas = node.firstChild; + return { + x0: a00 * ox + a01 * oy, + y0: a10 * ox + a11 * oy, + x1: a00 * x + a01 * y, + y1: a10 * x + a11 * y, + sin_th: sin_th, + cos_th: cos_th + }; + } + + function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) { + argsString = _join.call(arguments); + if (segmentToBezierCache[argsString]) { + return segmentToBezierCache[argsString]; } - else { - wrapper = fabric.document.createElement('span'); - wrapper.className = 'cufon cufon-vml'; - wrapper.alt = text; - canvas = fabric.document.createElement('span'); - canvas.className = 'cufon-vml-canvas'; - wrapper.appendChild(canvas); + var a00 = cos_th * rx; + var a01 = -sin_th * ry; + var a10 = sin_th * rx; + var a11 = cos_th * ry; - if (options.printable) { - var print = fabric.document.createElement('span'); - print.className = 'cufon-alt'; - print.appendChild(fabric.document.createTextNode(text)); - wrapper.appendChild(print); + var th_half = 0.5 * (th1 - th0); + var t = (8/3) * Math.sin(th_half * 0.5) * + Math.sin(th_half * 0.5) / Math.sin(th_half); + + var x1 = cx + Math.cos(th0) - t * Math.sin(th0); + var y1 = cy + Math.sin(th0) + t * Math.cos(th0); + var x3 = cx + Math.cos(th1); + var y3 = cy + Math.sin(th1); + var x2 = x3 + t * Math.sin(th1); + var y2 = y3 - t * Math.cos(th1); + + segmentToBezierCache[argsString] = [ + a00 * x1 + a01 * y1, a10 * x1 + a11 * y1, + a00 * x2 + a01 * y2, a10 * x2 + a11 * y2, + a00 * x3 + a01 * y3, a10 * x3 + a11 * y3 + ]; + + return segmentToBezierCache[argsString]; + } + + /** + * Draws arc + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Array} coords + */ + fabric.util.drawArc = function(ctx, x, y, coords) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } + else if (n !== 0 && n !== Number.POSITIVE_INFINITY && n !== Number.NEGATIVE_INFINITY) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } - // ie6, for some reason, has trouble rendering the last VML element in the document. - // we can work around this by injecting a dummy element where needed. - // @todo find a better solution - if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); - } + if (!Array.prototype.forEach) { + /** + * Iterates an array, invoking callback for each element + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.forEach = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + fn.call(context, this[i], i, this); + } + } + }; + } - var wStyle = wrapper.style; - var cStyle = canvas.style; + if (!Array.prototype.map) { + /** + * Returns a result of iterating over an array, invoking callback for each element + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.map = function(fn, context) { + var result = [ ]; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + result[i] = fn.call(context, this[i], i, this); + } + } + return result; + }; + } - var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); - var roundingFactor = roundedHeight / height; - var minX = viewBox.minX, minY = viewBox.minY; + if (!Array.prototype.every) { + /** + * Returns true if a callback returns truthy value for all elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Boolean} + */ + Array.prototype.every = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && !fn.call(context, this[i], i, this)) { + return false; + } + } + return true; + }; + } - cStyle.height = roundedHeight; - cStyle.top = Math.round(size.convert(minY - font.ascent)); - cStyle.left = Math.round(size.convert(minX)); + if (!Array.prototype.some) { + /** + * Returns true if a callback returns truthy value for at least one element in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Boolean} + */ + Array.prototype.some = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && fn.call(context, this[i], i, this)) { + return true; + } + } + return false; + }; + } - wStyle.height = size.convert(font.height) + 'px'; + if (!Array.prototype.filter) { + /** + * Returns the result of iterating over elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.filter = function(fn, context) { + var result = [ ], val; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + val = this[i]; // in case fn mutates this + if (fn.call(context, val, i, this)) { + result.push(val); + } + } + } + return result; + }; + } - var textDecoration = Cufon.getTextDecoration(options); + if (!Array.prototype.reduce) { + /** + * Returns "folded" (reduced) result of iterating over elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Any} + */ + Array.prototype.reduce = function(fn /*, initial*/) { + var len = this.length >>> 0, + i = 0, + rv; - var color = style.get('color'); - - var chars = Cufon.CSS.textTransform(text, style).split(''); - - var width = 0, offsetX = 0, advance = null; - - var glyph, shape, shadows = options.textShadow; - - // pre-calculate width - for (var i = 0, k = 0, l = chars.length; i < l; ++i) { - glyph = font.glyphs[chars[i]] || font.missingGlyph; - if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; - } - - if (advance === null) return null; - - var fullWidth = -minX + width + (viewBox.width - advance); - - var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); - - var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; - var stretch = 'r' + coordSize + 'nsnf'; - - for (i = 0; i < l; ++i) { - - glyph = font.glyphs[chars[i]] || font.missingGlyph; - if (!glyph) continue; - - if (redraw) { - // some glyphs may be missing so we can't use i - shape = canvas.childNodes[k]; - if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow + if (arguments.length > 1) { + rv = arguments[1]; } else { - shape = fabric.document.createElement('cvml:shape'); - canvas.appendChild(shape); - } - - shape.stroked = 'f'; - shape.coordsize = coordSize; - shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; - shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; - shape.fillcolor = color; - - // it's important to not set top/left or IE8 will grind to a halt - var sStyle = shape.style; - sStyle.width = roundedShapeWidth; - sStyle.height = roundedHeight; - - if (shadows) { - // due to the limitations of the VML shadow element there - // can only be two visible shadows. opacity is shared - // for all shadows. - var shadow1 = shadows[0], shadow2 = shadows[1]; - var color1 = Cufon.CSS.color(shadow1.color), color2; - var shadow = fabric.document.createElement('cvml:shadow'); - shadow.on = 't'; - shadow.color = color1.color; - shadow.offset = shadow1.offX + ',' + shadow1.offY; - if (shadow2) { - color2 = Cufon.CSS.color(shadow2.color); - shadow.type = 'double'; - shadow.color2 = color2.color; - shadow.offset2 = shadow2.offX + ',' + shadow2.offY; + do { + if (i in this) { + rv = this[i++]; + break; + } + // if array contains no values, no initial value to return + if (++i >= len) { + throw new TypeError(); + } } - shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; - shape.appendChild(shadow); + while (true); } + for (; i < len; i++) { + if (i in this) { + rv = fn.call(null, rv, this[i], i, this); + } + } + return rv; + }; + } - offsetX += ~~(glyph.w || font.w) + letterSpacing; - - ++k; + /* _ES5_COMPAT_END_ */ + /** + * Invokes method on all items in a given array + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + * @return {Array} + */ + function invoke(array, method) { + var args = slice.call(arguments, 2), result = [ ]; + for (var i = 0, len = array.length; i < len; i++) { + result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); } + return result; + } - wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); + /** + * Finds maximum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {Any} + */ + function max(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); + } - return wrapper; + /** + * Finds minimum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {Any} + */ + function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + /** + * @private + */ + function find(array, byProperty, condition) { + if (!array || array.length === 0) return undefined; + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + if (byProperty) { + while (i--) { + if (condition(array[i][byProperty], result)) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (condition(array[i], result)) { + result = array[i]; + } + } + } + return result; + } + + /** + * @namespace fabric.util.array + */ + fabric.util.array = { + invoke: invoke, + min: min, + max: max }; -})()); +})(); -Cufon.getTextDecoration = function(options) { - return { - underline: options.textDecoration === 'underline', - overline: options.textDecoration === 'overline', - 'line-through': options.textDecoration === 'line-through' + +(function(){ + + /** + * Copies all enumerable properties of one object to another + * @memberOf fabric.util.object + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + * @return {Object} + */ + function extend(destination, source) { + // JScript DontEnum bug is not taken care of + for (var property in source) { + destination[property] = source[property]; + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * @memberOf fabric.util.object + * @param {Object} object Object to clone + * @return {Object} + */ + function clone(object) { + return extend({ }, object); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone }; + +})(); + + +(function() { + +/* _ES5_COMPAT_START_ */ +if (!String.prototype.trim) { + /** + * Trims a string (removing whitespace from the beginning and the end) + * @function external:String#trim + * @see String#trim on MDN + */ + String.prototype.trim = function () { + // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now + return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); + }; +} +/* _ES5_COMPAT_END_ */ + +/** + * Camelizes a string + * @memberOf fabric.util.string + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ +function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); +} + +/** + * Capitalizes a string + * @memberOf fabric.util.string + * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. + * @return {String} Capitalized version of a string + */ +function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); +} + +/** + * Escapes XML in a string + * @memberOf fabric.util.string + * @param {String} string String to escape + * @return {String} Escaped version of a string + */ +function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +} + +/** + * String utilities + * @namespace fabric.util.string + */ +fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml }; - -if (typeof exports != 'undefined') { - exports.Cufon = Cufon; -} - - -/* - json2.js - 2011-10-19 - - Public Domain. - - NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. - - See http://www.JSON.org/js.html - - - This code should be minified before deployment. - See http://javascript.crockford.com/jsmin.html - - USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO - NOT CONTROL. - - - This file creates a global JSON object containing two methods: stringify - and parse. - - JSON.stringify(value, replacer, space) - value any JavaScript value, usually an object or array. - - replacer an optional parameter that determines how object - values are stringified for objects. It can be a - function or an array of strings. - - space an optional parameter that specifies the indentation - of nested structures. If it is omitted, the text will - be packed without extra whitespace. If it is a number, - it will specify the number of spaces to indent at each - level. If it is a string (such as '\t' or ' '), - it contains the characters used to indent at each level. - - This method produces a JSON text from a JavaScript value. - - When an object value is found, if the object contains a toJSON - method, its toJSON method will be called and the result will be - stringified. A toJSON method does not serialize: it returns the - value represented by the name/value pair that should be serialized, - or undefined if nothing should be serialized. The toJSON method - will be passed the key associated with the value, and this will be - bound to the value - - For example, this would serialize Dates as ISO strings. - - Date.prototype.toJSON = function (key) { - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - return this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z'; - }; - - You can provide an optional replacer method. It will be passed the - key and value of each member, with this bound to the containing - object. The value that is returned from your method will be - serialized. If your method returns undefined, then the member will - be excluded from the serialization. - - If the replacer parameter is an array of strings, then it will be - used to select the members to be serialized. It filters the results - such that only members with keys listed in the replacer array are - stringified. - - Values that do not have JSON representations, such as undefined or - functions, will not be serialized. Such values in objects will be - dropped; in arrays they will be replaced with null. You can use - a replacer function to replace those with JSON values. - JSON.stringify(undefined) returns undefined. - - The optional space parameter produces a stringification of the - value that is filled with line breaks and indentation to make it - easier to read. - - If the space parameter is a non-empty string, then that string will - be used for indentation. If the space parameter is a number, then - the indentation will be that many spaces. - - Example: - - text = JSON.stringify(['e', {pluribus: 'unum'}]); - // text is '["e",{"pluribus":"unum"}]' - - - text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); - // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' - - text = JSON.stringify([new Date()], function (key, value) { - return this[key] instanceof Date ? - 'Date(' + this[key] + ')' : value; - }); - // text is '["Date(---current time---)"]' - - - JSON.parse(text, reviver) - This method parses a JSON text to produce an object or array. - It can throw a SyntaxError exception. - - The optional reviver parameter is a function that can filter and - transform the results. It receives each of the keys and values, - and its return value is used instead of the original value. - If it returns what it received, then the structure is not modified. - If it returns undefined then the member is deleted. - - Example: - - // Parse the text. Values that look like ISO date strings will - // be converted to Date objects. - - myData = JSON.parse(text, function (key, value) { - var a; - if (typeof value === 'string') { - a = -/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); - if (a) { - return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], - +a[5], +a[6])); - } - } - return value; - }); - - myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { - var d; - if (typeof value === 'string' && - value.slice(0, 5) === 'Date(' && - value.slice(-1) === ')') { - d = new Date(value.slice(5, -1)); - if (d) { - return d; - } - } - return value; - }); - - - This is a reference implementation. You are free to copy, modify, or - redistribute. -*/ - -/*jslint evil: true, regexp: true */ - -/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, - call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, - getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, - lastIndex, length, parse, prototype, push, replace, slice, stringify, - test, toJSON, toString, valueOf -*/ - - -// Create a JSON object only if one does not already exist. We create the -// methods in a closure to avoid creating global variables. - -var JSON; -if (!JSON) { - JSON = {}; -} - -(function () { - 'use strict'; - - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - if (typeof Date.prototype.toJSON !== 'function') { - - /** @ignore */ - Date.prototype.toJSON = function (key) { - - return isFinite(this.valueOf()) - ? this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z' - : null; - }; - - String.prototype.toJSON = - Number.prototype.toJSON = - /** @ignore */ - Boolean.prototype.toJSON = function (key) { - return this.valueOf(); - }; - } - - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; - - - function quote(string) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - escapable.lastIndex = 0; - return escapable.test(string) ? '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' - ? c - : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : '"' + string + '"'; - } - - - function str(key, holder) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 - ? '[]' - : gap - ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' - : '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 - ? '{}' - : gap - ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' - : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - if (typeof JSON.stringify !== 'function') { - /** @ignore */ - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - } - - -// If the JSON object does not yet have a parse method, give it one. - - if (typeof JSON.parse !== 'function') { - /** @ignore */ - JSON.parse = function (text, reviver) { - -// The parse method takes a text and an optional reviver function, and returns -// a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - -// The walk method is used to recursively walk the resulting structure so -// that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - -// Parsing happens in four stages. In the first stage, we replace certain -// Unicode characters with escape sequences. JavaScript handles many characters -// incorrectly, either silently deleting them, or treating them as line endings. - - text = String(text); - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - -// In the second stage, we run the text against regular expressions that look -// for non-JSON patterns. We are especially concerned with '()' and 'new' -// because they can cause invocation, and '=' because it can cause mutation. -// But just to be safe, we want to reject all unexpected forms. - -// We split the second stage into 4 regexp operations in order to work around -// crippling inefficiencies in IE's and Safari's regexp engines. First we -// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we -// replace all simple value tokens with ']' characters. Third, we delete all -// open brackets that follow a colon or comma or that begin the text. Finally, -// we look to see that the remaining characters are only whitespace or ']' or -// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - if (/^[\],:{}\s]*$/ - .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') - .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - -// In the third stage we use the eval function to compile the text into a -// JavaScript structure. The '{' operator is subject to a syntactic ambiguity -// in JavaScript: it can begin a block or an object literal. We wrap the text -// in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - -// In the optional fourth stage, we recursively walk the new structure, passing -// each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' - ? walk({'': j}, '') - : j; - } - -// If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - } }()); + +/* _ES5_COMPAT_START_ */ +(function() { + + var slice = Array.prototype.slice, + apply = Function.prototype.apply, + Dummy = function() { }; + + if (!Function.prototype.bind) { + /** + * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) + * @see Function#bind on MDN + * @param {Object} thisArg Object to bind function to + * @param {Any[]} [...] Values to pass to a bound function + * @return {Function} + */ + Function.prototype.bind = function(thisArg) { + var fn = this, args = slice.call(arguments, 1), bound; + if (args.length) { + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); + }; + } + else { + /** @ignore */ + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); + }; + } + Dummy.prototype = this.prototype; + bound.prototype = new Dummy(); + + return bound; + }; + } + +})(); +/* _ES5_COMPAT_END_ */ + + +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }; + + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; + } + return true; + })(); + + /** @ignore */ + var addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + function callSuper(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param parent optional "Class" to inherit from + * @param properties Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = [ ]; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; + } + + fabric.util.createClass = createClass; +})(); + + +(function () { + + var unknown = 'unknown'; + + /* EVENT HANDLING */ + + function areHostMethods(object) { + var methodNames = Array.prototype.slice.call(arguments, 1), + t, i, len = methodNames.length; + for (i = 0; i < len; i++) { + t = typeof object[methodNames[i]]; + if (!(/^(?:function|object|unknown)$/).test(t)) return false; + } + return true; + } + var getUniqueId = (function () { + var uid = 0; + return function (element) { + return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); + }; + })(); + + /** @ignore */ + var getElement, setElement; + + (function () { + var elements = { }; + /** @ignore */ + getElement = function (uid) { + return elements[uid]; + }; + /** @ignore */ + setElement = function (uid, element) { + elements[uid] = element; + }; + })(); + + function createListener(uid, handler) { + return { + handler: handler, + wrappedHandler: createWrappedHandler(uid, handler) + }; + } + + function createWrappedHandler(uid, handler) { + return function (e) { + handler.call(getElement(uid), e || fabric.window.event); + }; + } + + function createDispatcher(uid, eventName) { + return function (e) { + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + handlersForEvent[i].call(this, e || fabric.window.event); + } + } + }; + } + + var shouldUseAddListenerRemoveListener = ( + areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && + areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), + + shouldUseAttachEventDetachEvent = ( + areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && + areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), + + // IE branch + listeners = { }, + +<<<<<<< HEAD /* ---------------------------------------------------- Event.js : 1.1.1 : 2012/11/19 : MIT License @@ -3660,1416 +3426,10 @@ Event.proxy = (function(root) { * @param {Any} values Values to log */ fabric.log = function() { }; - -/** - * Wrapper around `console.warn` (when available) - * @param {Any} Values to log as a warning - */ -fabric.warn = function() { }; - -if (typeof console !== 'undefined') { - if (typeof console.log !== 'undefined' && console.log.apply) { - fabric.log = function() { - return console.log.apply(console, arguments); - }; - } - if (typeof console.warn !== 'undefined' && console.warn.apply) { - fabric.warn = function() { - return console.warn.apply(console, arguments); - }; - } -} - - -(function(){ - - /** - * @private - * @param {String} eventName - * @param {Function} handler - */ - function _removeEventListener(eventName, handler) { - if (!this.__eventListeners[eventName]) return; - - if (handler) { - fabric.util.removeFromArray(this.__eventListeners[eventName], handler); - } - else { - this.__eventListeners[eventName].length = 0; - } - } - - /** - * Observes specified event - * @deprecated `observe` deprecated since 0.8.34 (use `on` instead) - * @memberOf fabric.Observable - * @alias on - * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) - * @param {Function} handler Function that receives a notification when an event of the specified type occurs - * @return {Self} thisArg - * @chainable - */ - function observe(eventName, handler) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - // one object with key/value pairs was passed - if (arguments.length === 1) { - for (var prop in eventName) { - this.on(prop, eventName[prop]); - } - } - else { - if (!this.__eventListeners[eventName]) { - this.__eventListeners[eventName] = [ ]; - } - this.__eventListeners[eventName].push(handler); - } - return this; - } - - /** - * Stops event observing for a particular event handler - * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead) - * @memberOf fabric.Observable - * @alias off - * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) - * @param {Function} handler Function to be deleted from EventListeners - * @return {Self} thisArg - * @chainable - */ - function stopObserving(eventName, handler) { - if (!this.__eventListeners) return; - - // one object with key/value pairs was passed - if (arguments.length === 1 && typeof arguments[0] === 'object') { - for (var prop in eventName) { - _removeEventListener.call(this, prop, eventName[prop]); - } - } - else { - _removeEventListener.call(this, eventName, handler); - } - return this; - } - - /** - * Fires event with an optional options object - * @deprecated `fire` deprecated since 1.0.7 (use `trigger` instead) - * @memberOf fabric.Observable - * @alias trigger - * @param {String} eventName Event name to fire - * @param {Object} [options] Options object - * @return {Self} thisArg - * @chainable - */ - function fire(eventName, options) { - if (!this.__eventListeners) return; - - var listenersForEvent = this.__eventListeners[eventName]; - if (!listenersForEvent) return; - for (var i = 0, len = listenersForEvent.length; i < len; i++) { - // avoiding try/catch for perf. reasons - listenersForEvent[i].call(this, options || { }); - } - return this; - } - - /** - * @namespace fabric.Observable - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#events} - * @see {@link http://fabricjs.com/events/|Events demo} - */ - fabric.Observable = { - observe: observe, - stopObserving: stopObserving, - fire: fire, - - on: observe, - off: stopObserving, - trigger: fire - }; -})(); - - -/** - * @namespace fabric.Collection - */ -fabric.Collection = { - - /** - * Adds objects to collection, then renders canvas (if `renderOnAddRemove` is not `false`) - * Objects should be instances of (or inherit from) fabric.Object - * @param [...] Zero or more fabric instances - * @return {Self} thisArg - */ - add: function () { - this._objects.push.apply(this._objects, arguments); - for (var i = arguments.length; i--; ) { - this._onObjectAdded(arguments[i]); - } - this.renderOnAddRemove && this.renderAll(); - return this; - }, - - /** - * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) - * An object should be an instance of (or inherit from) fabric.Object - * @param {Object} object Object to insert - * @param {Number} index Index to insert object at - * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs - * @return {Self} thisArg - */ - insertAt: function (object, index, nonSplicing) { - var objects = this.getObjects(); - if (nonSplicing) { - objects[index] = object; - } - else { - objects.splice(index, 0, object); - } - this._onObjectAdded(object); - this.renderOnAddRemove && this.renderAll(); - return this; - }, - - /** - * Removes an object from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) - * @param {Object} object Object to remove - * @return {Self} thisArg - */ - remove: function(object) { - var objects = this.getObjects(), - index = objects.indexOf(object); - - // only call onObjectRemoved if an object was actually removed - if (index !== -1) { - objects.splice(index, 1); - this._onObjectRemoved(object); - } - - this.renderOnAddRemove && this.renderAll(); - return object; - }, - - /** - * Executes given function for each object in this group - * @param {Function} callback - * Callback invoked with current object as first argument, - * index - as second and an array of all objects - as third. - * Iteration happens in reverse order (for performance reasons). - * Callback is invoked in a context of Global Object (e.g. `window`) - * when no `context` argument is given - * - * @param {Object} context Context (aka thisObject) - * @return {Self} thisArg - */ - forEachObject: function(callback, context) { - var objects = this.getObjects(), - i = objects.length; - while (i--) { - callback.call(context, objects[i], i, objects); - } - return this; - }, - - /** - * Returns object at specified index - * @param {Number} index - * @return {Self} thisArg - */ - item: function (index) { - return this.getObjects()[index]; - }, - - /** - * Returns true if collection contains no objects - * @return {Boolean} true if collection is empty - */ - isEmpty: function () { - return this.getObjects().length === 0; - }, - - /** - * Returns a size of a collection (i.e: length of an array containing its objects) - * @return {Number} Collection size - */ - size: function() { - return this.getObjects().length; - }, - - /** - * Returns true if collection contains an object - * @param {Object} object Object to check against - * @return {Boolean} `true` if collection contains an object - */ - contains: function(object) { - return this.getObjects().indexOf(object) > -1; - }, - - /** - * Returns number representation of a collection complexity - * @return {Number} complexity - */ - complexity: function () { - return this.getObjects().reduce(function (memo, current) { - memo += current.complexity ? current.complexity() : 0; - return memo; - }, 0); - } -}; - - -(function(global) { - - var sqrt = Math.sqrt, - atan2 = Math.atan2; - - /** - * @namespace fabric.util - */ - fabric.util = { }; - - /** - * Removes value from an array. - * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` - * @static - * @memberOf fabric.util - * @param {Array} array - * @param {Any} value - * @return {Array} original array - */ - function removeFromArray(array, value) { - var idx = array.indexOf(value); - if (idx !== -1) { - array.splice(idx, 1); - } - return array; - } - - /** - * Returns random number between 2 specified ones. - * @static - * @memberOf fabric.util - * @param {Number} min lower limit - * @param {Number} max upper limit - * @return {Number} random value (between min and max) - */ - function getRandomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; - } - - var PiBy180 = Math.PI / 180; - - /** - * Transforms degrees to radians. - * @static - * @memberOf fabric.util - * @param {Number} degrees value in degrees - * @return {Number} value in radians - */ - function degreesToRadians(degrees) { - return degrees * PiBy180; - } - - /** - * Transforms radians to degrees. - * @static - * @memberOf fabric.util - * @param {Number} radians value in radians - * @return {Number} value in degrees - */ - function radiansToDegrees(radians) { - return radians / PiBy180; - } - - /** - * Rotates `point` around `origin` with `radians` - * @static - * @memberOf fabric.util - * @param {fabric.Point} The point to rotate - * @param {fabric.Point} The origin of the rotation - * @param {Number} The radians of the angle for the rotation - * @return {fabric.Point} The new rotated point - */ - function rotatePoint(point, origin, radians) { - var sin = Math.sin(radians), - cos = Math.cos(radians); - - point.subtractEquals(origin); - - var rx = point.x * cos - point.y * sin; - var ry = point.x * sin + point.y * cos; - - return new fabric.Point(rx, ry).addEquals(origin); - } - - /** - * Apply transform t to point p - * @static - * @memberOf fabric.util - * @param {fabric.Point} p The point to transform - * @param {Array} t The transform - * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied - * @return {fabric.Point} The transformed point - */ - function transformPoint(p, t, ignoreOffset) { - if (ignoreOffset) { - return new fabric.Point( - t[0] * p.x + t[1] * p.y, - t[2] * p.x + t[3] * p.y - ); - } - return new fabric.Point( - t[0] * p.x + t[1] * p.y + t[4], - t[2] * p.x + t[3] * p.y + t[5] - ); - } - - /** - * Invert transformation t - * @static - * @memberOf fabric.util - * @param {Array} t The transform - * @return {Array} The inverted transform - */ - function invertTransform(t) { - var r = t.slice(), - a = 1 / (t[0] * t[3] - t[1] * t[2]); - r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; - var o = transformPoint({x: t[4], y: t[5]}, r); - r[4] = -o.x; - r[5] = -o.y; - return r - } - - /** - * A wrapper around Number#toFixed, which contrary to native method returns number, not string. - * @static - * @memberOf fabric.util - * @param {Number | String} number number to operate on - * @param {Number} fractionDigits number of fraction digits to "leave" - * @return {Number} - */ - function toFixed(number, fractionDigits) { - return parseFloat(Number(number).toFixed(fractionDigits)); - } - - /** - * Function which always returns `false`. - * @static - * @memberOf fabric.util - * @return {Boolean} - */ - function falseFunction() { - return false; - } - - /** - * Returns klass "Class" object of given namespace - * @memberOf fabric.util - * @param {String} type Type of object (eg. 'circle') - * @param {String} namespace Namespace to get klass "Class" object from - * @return {Object} klass "Class" - */ - function getKlass(type, namespace) { - // capitalize first letter only - type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); - return resolveNamespace(namespace)[type]; - } - - /** - * Returns object of given namespace - * @memberOf fabric.util - * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' - * @return {Object} Object for given namespace (default fabric) - */ - function resolveNamespace(namespace) { - if (!namespace) return fabric; - - var parts = namespace.split('.'), - len = parts.length, - obj = global || fabric.window; - - for (var i = 0; i < len; ++i) { - obj = obj[parts[i]]; - } - - return obj; - } - - /** - * Loads image element from given url and passes it to a callback - * @memberOf fabric.util - * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {Any} context optional Context to invoke callback in - */ - function loadImage(url, callback, context) { - if (url) { - var img = fabric.util.createImage(); - /** @ignore */ - img.onload = function () { - callback && callback.call(context, img); - img = img.onload = null; - }; - img.src = url; - } - else { - callback && callback.call(context, url); - } - } - - /** - * Creates corresponding fabric instances from their object representations - * @static - * @memberOf fabric.util - * @param {Array} objects Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * @param {Function} [reviver] Method for further parsing of object elements, called after each fabric object created. - */ - function enlivenObjects(objects, callback, namespace, reviver) { - - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - if (callback) { - callback(enlivenedObjects); - } - } - } - - var enlivenedObjects = [ ], - numLoadedObjects = 0, - numTotalObjects = objects.length; - - objects.forEach(function (o, index) { - // if sparse array - if (!o || !o.type) { - onLoaded(); - return; - } - var klass = fabric.util.getKlass(o.type, namespace); - if (klass.async) { - klass.fromObject(o, function (obj, error) { - if (!error) { - enlivenedObjects[index] = obj; - reviver && reviver(o, enlivenedObjects[index]); - } - onLoaded(); - }); - } - else { - enlivenedObjects[index] = klass.fromObject(o); - reviver && reviver(o, enlivenedObjects[index]); - onLoaded(); - } - }); - } - - /** - * Groups SVG elements (usually those retrieved from SVG document) - * @static - * @memberOf fabric.util - * @param {Array} elements SVG elements to group - * @param {Object} [options] Options object - * @return {fabric.Object|fabric.PathGroup} - */ - function groupSVGElements(elements, options, path) { - var object; - - if (elements.length > 1) { - object = new fabric.PathGroup(elements, options); - } - else { - object = elements[0]; - } - - if (typeof path !== 'undefined') { - object.setSourcePath(path); - } - return object; - } - - /** - * Populates an object with properties of another object - * @static - * @memberOf fabric.util - * @param {Object} source Source object - * @param {Object} destination Destination object - * @return {Array} properties Propertie names to include - */ - function populateWithProperties(source, destination, properties) { - if (properties && Object.prototype.toString.call(properties) === '[object Array]') { - for (var i = 0, len = properties.length; i < len; i++) { - if (properties[i] in source) { - destination[properties[i]] = source[properties[i]]; - } - } - } - } - - /** - * Draws a dashed line between two points - * - * This method is used to draw dashed line around selection area. - * See dotted stroke in canvas - * - * @param ctx {Canvas} context - * @param x {Number} start x coordinate - * @param y {Number} start y coordinate - * @param x2 {Number} end x coordinate - * @param y2 {Number} end y coordinate - * @param da {Array} dash array pattern - */ - function drawDashedLine(ctx, x, y, x2, y2, da) { - var dx = x2 - x, - dy = y2 - y, - len = sqrt(dx*dx + dy*dy), - rot = atan2(dy, dx), - dc = da.length, - di = 0, - draw = true; - - ctx.save(); - ctx.translate(x, y); - ctx.moveTo(0, 0); - ctx.rotate(rot); - - x = 0; - while (len > x) { - x += da[di++ % dc]; - if (x > len) { - x = len; - } - ctx[draw ? 'lineTo' : 'moveTo'](x, 0); - draw = !draw; - } - - ctx.restore(); - } - - /** - * Creates canvas element and initializes it via excanvas if necessary - * @static - * @memberOf fabric.util - * @param {CanvasElement} [canvasEl] optional canvas element to initialize; when not given, element is created implicitly - * @return {CanvasElement} initialized canvas element - */ - function createCanvasElement(canvasEl) { - canvasEl || (canvasEl = fabric.document.createElement('canvas')); - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - return canvasEl; - } - - /** - * Creates image element (works on client and node) - * @static - * @memberOf fabric.util - * @return {HTMLImageElement} HTML image element - */ - function createImage() { - return fabric.isLikelyNode - ? new (require('canvas').Image)() - : fabric.document.createElement('img'); - } - - /** - * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array - * @static - * @memberOf fabric.util - * @param {Object} klass "Class" to create accessors for - */ - function createAccessors(klass) { - var proto = klass.prototype; - - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - } - - /** - * @static - * @memberOf fabric.util - * @param {fabric.Object} receiver Object implementing `clipTo` method - * @param {CanvasRenderingContext2D} ctx Context to clip - */ - function clipContext(receiver, ctx) { - ctx.save(); - ctx.beginPath(); - receiver.clipTo(ctx); - ctx.clip(); - } - - /** - * Multiply matrix A by matrix B to nest transformations - * @static - * @memberOf fabric.util - * @param {Array} matrixA First transformMatrix - * @param {Array} matrixB Second transformMatrix - * @return {Array} The product of the two transform matrices - */ - function multiplyTransformMatrices(matrixA, matrixB) { - // Matrix multiply matrixA * matrixB - var a = [ - [matrixA[0], matrixA[2], matrixA[4]], - [matrixA[1], matrixA[3], matrixA[5]], - [0 , 0 , 1 ] - ]; - - var b = [ - [matrixB[0], matrixB[2], matrixB[4]], - [matrixB[1], matrixB[3], matrixB[5]], - [0 , 0 , 1 ] - ]; - - var result = []; - for (var r=0; r<3; r++) { - result[r] = []; - for (var c=0; c<3; c++) { - var sum = 0; - for (var k=0; k<3; k++) { - sum += a[r][k]*b[k][c]; - } - - result[r][c] = sum; - } - } - - return [ - result[0][0], - result[1][0], - result[0][1], - result[1][1], - result[0][2], - result[1][2] - ]; - } - - function getFunctionBody(fn) { - return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; - } - - function drawArc(ctx, x, y, coords) { - var rx = coords[0]; - var ry = coords[1]; - var rot = coords[2]; - var large = coords[3]; - var sweep = coords[4]; - var ex = coords[5]; - var ey = coords[6]; - var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); - for (var i=0; i 1) { - pl = Math.sqrt(pl); - rx *= pl; - ry *= pl; - } - - var a00 = cos_th / rx; - var a01 = sin_th / rx; - var a10 = (-sin_th) / ry; - var a11 = (cos_th) / ry; - var x0 = a00 * ox + a01 * oy; - var y0 = a10 * ox + a11 * oy; - var x1 = a00 * x + a01 * y; - var y1 = a10 * x + a11 * y; - - var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); - var sfactor_sq = 1 / d - 0.25; - if (sfactor_sq < 0) sfactor_sq = 0; - var sfactor = Math.sqrt(sfactor_sq); - if (sweep === large) sfactor = -sfactor; - var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); - var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); - - var th0 = Math.atan2(y0-yc, x0-xc); - var th1 = Math.atan2(y1-yc, x1-xc); - - var th_arc = th1-th0; - if (th_arc < 0 && sweep === 1){ - th_arc += 2*Math.PI; - } else if (th_arc > 0 && sweep === 0) { - th_arc -= 2 * Math.PI; - } - - var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); - var result = []; - for (var i=0; i>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 0) { - n = Number(arguments[1]); - if (n !== n) { // shortcut for verifying if it's NaN - n = 0; - } - else if (n !== 0 && n !== Number.POSITIVE_INFINITY && n !== Number.NEGATIVE_INFINITY) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - }; - } - - if (!Array.prototype.forEach) { - /** - * Iterates an array, invoking callback for each element - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Array} - */ - Array.prototype.forEach = function(fn, context) { - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this) { - fn.call(context, this[i], i, this); - } - } - }; - } - - if (!Array.prototype.map) { - /** - * Returns a result of iterating over an array, invoking callback for each element - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Array} - */ - Array.prototype.map = function(fn, context) { - var result = [ ]; - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this) { - result[i] = fn.call(context, this[i], i, this); - } - } - return result; - }; - } - - if (!Array.prototype.every) { - /** - * Returns true if a callback returns truthy value for all elements in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Boolean} - */ - Array.prototype.every = function(fn, context) { - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this && !fn.call(context, this[i], i, this)) { - return false; - } - } - return true; - }; - } - - if (!Array.prototype.some) { - /** - * Returns true if a callback returns truthy value for at least one element in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Boolean} - */ - Array.prototype.some = function(fn, context) { - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this && fn.call(context, this[i], i, this)) { - return true; - } - } - return false; - }; - } - - if (!Array.prototype.filter) { - /** - * Returns the result of iterating over elements in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Array} - */ - Array.prototype.filter = function(fn, context) { - var result = [ ], val; - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this) { - val = this[i]; // in case fn mutates this - if (fn.call(context, val, i, this)) { - result.push(val); - } - } - } - return result; - }; - } - - if (!Array.prototype.reduce) { - /** - * Returns "folded" (reduced) result of iterating over elements in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Any} - */ - Array.prototype.reduce = function(fn /*, initial*/) { - var len = this.length >>> 0, - i = 0, - rv; - - if (arguments.length > 1) { - rv = arguments[1]; - } - else { - do { - if (i in this) { - rv = this[i++]; - break; - } - // if array contains no values, no initial value to return - if (++i >= len) { - throw new TypeError(); - } - } - while (true); - } - for (; i < len; i++) { - if (i in this) { - rv = fn.call(null, rv, this[i], i, this); - } - } - return rv; - }; - } - - /* _ES5_COMPAT_END_ */ - - /** - * Invokes method on all items in a given array - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} method Name of a method to invoke - * @return {Array} - */ - function invoke(array, method) { - var args = slice.call(arguments, 2), result = [ ]; - for (var i = 0, len = array.length; i < len; i++) { - result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); - } - return result; - } - - /** - * Finds maximum value in array (not necessarily "first" one) - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - * @return {Any} - */ - function max(array, byProperty) { - if (!array || array.length === 0) return undefined; - - var i = array.length - 1, - result = byProperty ? array[i][byProperty] : array[i]; - if (byProperty) { - while (i--) { - if (array[i][byProperty] >= result) { - result = array[i][byProperty]; - } - } - } - else { - while (i--) { - if (array[i] >= result) { - result = array[i]; - } - } - } - return result; - } - - /** - * Finds minimum value in array (not necessarily "first" one) - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - * @return {Any} - */ - function min(array, byProperty) { - if (!array || array.length === 0) return undefined; - - var i = array.length - 1, - result = byProperty ? array[i][byProperty] : array[i]; - - if (byProperty) { - while (i--) { - if (array[i][byProperty] < result) { - result = array[i][byProperty]; - } - } - } - else { - while (i--) { - if (array[i] < result) { - result = array[i]; - } - } - } - return result; - } - - /** - * @namespace fabric.util.array - */ - fabric.util.array = { - invoke: invoke, - min: min, - max: max - }; - -})(); - - -(function(){ - - /** - * Copies all enumerable properties of one object to another - * @memberOf fabric.util.object - * @param {Object} destination Where to copy to - * @param {Object} source Where to copy from - * @return {Object} - */ - function extend(destination, source) { - // JScript DontEnum bug is not taken care of - for (var property in source) { - destination[property] = source[property]; - } - return destination; - } - - /** - * Creates an empty object and copies all enumerable properties of another object to it - * @memberOf fabric.util.object - * @param {Object} object Object to clone - * @return {Object} - */ - function clone(object) { - return extend({ }, object); - } - - /** @namespace fabric.util.object */ - fabric.util.object = { - extend: extend, - clone: clone - }; - -})(); - - -(function() { - -/* _ES5_COMPAT_START_ */ -if (!String.prototype.trim) { - /** - * Trims a string (removing whitespace from the beginning and the end) - * @function external:String#trim - * @see String#trim on MDN - */ - String.prototype.trim = function () { - // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now - return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); - }; -} -/* _ES5_COMPAT_END_ */ - -/** - * Camelizes a string - * @memberOf fabric.util.string - * @param {String} string String to camelize - * @return {String} Camelized version of a string - */ -function camelize(string) { - return string.replace(/-+(.)?/g, function(match, character) { - return character ? character.toUpperCase() : ''; - }); -} - -/** - * Capitalizes a string - * @memberOf fabric.util.string - * @param {String} string String to capitalize - * @return {String} Capitalized version of a string - */ -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); -} - -/** - * Escapes XML in a string - * @memberOf fabric.util.string - * @param {String} string String to escape - * @return {String} Escaped version of a string - */ -function escapeXml(string) { - return string.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); -} - -/** - * String utilities - * @namespace fabric.util.string - */ -fabric.util.string = { - camelize: camelize, - capitalize: capitalize, - escapeXml: escapeXml -}; -}()); - - -/* _ES5_COMPAT_START_ */ -(function() { - - var slice = Array.prototype.slice, - apply = Function.prototype.apply, - Dummy = function() { }; - - if (!Function.prototype.bind) { - /** - * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) - * @see Function#bind on MDN - * @param {Object} thisArg Object to bind function to - * @param {Any[]} [...] Values to pass to a bound function - * @return {Function} - */ - Function.prototype.bind = function(thisArg) { - var fn = this, args = slice.call(arguments, 1), bound; - if (args.length) { - bound = function() { - return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); - }; - } - else { - /** @ignore */ - bound = function() { - return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); - }; - } - Dummy.prototype = this.prototype; - bound.prototype = new Dummy(); - - return bound; - }; - } - -})(); -/* _ES5_COMPAT_END_ */ - - -(function() { - - var slice = Array.prototype.slice, emptyFunction = function() { }; - - var IS_DONTENUM_BUGGY = (function(){ - for (var p in { toString: 1 }) { - if (p === 'toString') return false; - } - return true; - })(); - - /** @ignore */ - var addMethods = function(klass, source, parent) { - for (var property in source) { - - if (property in klass.prototype && - typeof klass.prototype[property] === 'function' && - (source[property] + '').indexOf('callSuper') > -1) { - - klass.prototype[property] = (function(property) { - return function() { - - var superclass = this.constructor.superclass; - this.constructor.superclass = parent; - var returnValue = source[property].apply(this, arguments); - this.constructor.superclass = superclass; - - if (property !== 'initialize') { - return returnValue; - } - }; - })(property); - } - else { - klass.prototype[property] = source[property]; - } - - if (IS_DONTENUM_BUGGY) { - if (source.toString !== Object.prototype.toString) { - klass.prototype.toString = source.toString; - } - if (source.valueOf !== Object.prototype.valueOf) { - klass.prototype.valueOf = source.valueOf; - } - } - } - }; - - function Subclass() { } - - function callSuper(methodName) { - var fn = this.constructor.superclass.prototype[methodName]; - return (arguments.length > 1) - ? fn.apply(this, slice.call(arguments, 1)) - : fn.call(this); - } - - /** - * Helper for creation of "classes". - * @memberOf fabric.util - * @param parent optional "Class" to inherit from - * @param properties Properties shared by all instances of this class - * (be careful modifying objects defined here as this would affect all instances) - */ - function createClass() { - var parent = null, - properties = slice.call(arguments, 0); - - if (typeof properties[0] === 'function') { - parent = properties.shift(); - } - function klass() { - this.initialize.apply(this, arguments); - } - - klass.superclass = parent; - klass.subclasses = [ ]; - - if (parent) { - Subclass.prototype = parent.prototype; - klass.prototype = new Subclass(); - parent.subclasses.push(klass); - } - for (var i = 0, length = properties.length; i < length; i++) { - addMethods(klass, properties[i], parent); - } - if (!klass.prototype.initialize) { - klass.prototype.initialize = emptyFunction; - } - klass.prototype.constructor = klass; - klass.prototype.callSuper = callSuper; - return klass; - } - - fabric.util.createClass = createClass; -})(); - - -(function () { - - /* EVENT HANDLING */ - - function areHostMethods(object) { - var methodNames = Array.prototype.slice.call(arguments, 1), - t, i, len = methodNames.length; - for (i = 0; i < len; i++) { - t = typeof object[methodNames[i]]; - if (!(/^(?:function|object|unknown)$/).test(t)) return false; - } - return true; - } - var getUniqueId = (function () { - var uid = 0; - return function (element) { - return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); - }; - })(); - - /** @ignore */ - var getElement, setElement; - - (function () { - var elements = { }; - /** @ignore */ - getElement = function (uid) { - return elements[uid]; - }; - /** @ignore */ - setElement = function (uid, element) { - elements[uid] = element; - }; - })(); - - function createListener(uid, handler) { - return { - handler: handler, - wrappedHandler: createWrappedHandler(uid, handler) - }; - } - - function createWrappedHandler(uid, handler) { - return function (e) { - handler.call(getElement(uid), e || fabric.window.event); - }; - } - - function createDispatcher(uid, eventName) { - return function (e) { - if (handlers[uid] && handlers[uid][eventName]) { - var handlersForEvent = handlers[uid][eventName]; - for (var i = 0, len = handlersForEvent.length; i < len; i++) { - handlersForEvent[i].call(this, e || fabric.window.event); - } - } - }; - } - - var shouldUseAddListenerRemoveListener = ( - areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && - areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), - - shouldUseAttachEventDetachEvent = ( - areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && - areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), - - // IE branch - listeners = { }, - +======= // DOM L0 branch handlers = { }, +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f addListener, removeListener; @@ -5174,41 +3534,14 @@ fabric.util.string = { function getPointer(event, upperCanvasEl) { event || (event = fabric.window.event); - var element = event.target || (typeof event.srcElement !== 'unknown' ? event.srcElement : null), - body = fabric.document.body || {scrollLeft: 0, scrollTop: 0}, - docElement = fabric.document.documentElement, - orgElement = element, - scrollLeft = 0, - scrollTop = 0, - firstFixedAncestor; + var element = event.target || + (typeof event.srcElement !== unknown ? event.srcElement : null); - while (element && element.parentNode && !firstFixedAncestor) { - element = element.parentNode; - - if (element !== fabric.document && - fabric.util.getElementStyle(element, 'position') === 'fixed') { - firstFixedAncestor = element; - } - - if (element !== fabric.document && - orgElement !== upperCanvasEl && - fabric.util.getElementStyle(element, 'position') === 'absolute') { - scrollLeft = 0; - scrollTop = 0; - } - else if (element === fabric.document) { - scrollLeft = body.scrollLeft || docElement.scrollLeft || 0; - scrollTop = body.scrollTop || docElement.scrollTop || 0; - } - else { - scrollLeft += element.scrollLeft || 0; - scrollTop += element.scrollTop || 0; - } - } + var scroll = fabric.util.getScrollLeftTop(element, upperCanvasEl); return { - x: pointerX(event) + scrollLeft, - y: pointerY(event) + scrollTop + x: pointerX(event) + scroll.left, + y: pointerY(event) + scroll.top }; } @@ -5216,29 +3549,28 @@ fabric.util.string = { // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] // need to investigate later - return (typeof event.clientX !== 'unknown' ? event.clientX : 0); + return (typeof event.clientX !== unknown ? event.clientX : 0); }; var pointerY = function(event) { - return (typeof event.clientY !== 'unknown' ? event.clientY : 0); + return (typeof event.clientY !== unknown ? event.clientY : 0); }; + function _getPointer(event, pageProp, clientProp) { + var touchProp = event.type === 'touchend' ? 'changedTouches' : 'touches'; + + return (event[touchProp] && event[touchProp][0] + ? (event[touchProp][0][pageProp] - (event[touchProp][0][pageProp] - event[touchProp][0][clientProp])) + || event[clientProp] + : event[clientProp]); + } + if (fabric.isTouchSupported) { pointerX = function(event) { - if (event.type !== 'touchend') { - return (event.touches && event.touches[0] ? - (event.touches[0].pageX - (event.touches[0].pageX - event.touches[0].clientX)) || event.clientX : event.clientX); - } - return (event.changedTouches && event.changedTouches[0] - ? (event.changedTouches[0].pageX - (event.changedTouches[0].pageX - event.changedTouches[0].clientX)) || event.clientX : event.clientX); + return _getPointer(event, 'pageX', 'clientX'); }; pointerY = function(event) { - if (event.type !== 'touchend') { - return (event.touches && event.touches[0] - ? (event.touches[0].pageY - (event.touches[0].pageY - event.touches[0].clientY)) || event.clientY : event.clientY); - } - return (event.changedTouches && event.changedTouches[0] - ? (event.changedTouches[0].pageY - (event.changedTouches[0].pageY - event.changedTouches[0].clientY)) || event.clientY : event.clientY); + return _getPointer(event, 'pageY', 'clientY'); }; } @@ -5395,9 +3727,53 @@ fabric.util.string = { element.className += (element.className ? ' ' : '') + className; } } + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + function transformPoint(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[1] * p.y, + t[2] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[1] * p.y + t[4], + t[2] * p.x + t[3] * p.y + t[5] + ); + } /** +<<<<<<< HEAD + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + function invertTransform(t) { + var r = t.slice(), + a = 1 / (t[0] * t[3] - t[1] * t[2]); + r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; + var o = transformPoint({x: t[4], y: t[5]}, r); + r[4] = -o.x; + r[5] = -o.y; + return r + } + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static +======= * Wraps element with another element +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f * @memberOf fabric.util * @param {HTMLElement} element Element to wrap * @param {HTMLElement|String} wrapper Element to wrap with @@ -5414,6 +3790,47 @@ fabric.util.string = { wrapper.appendChild(element); return wrapper; } + + function getScrollLeftTop(element, upperCanvasEl) { + + var firstFixedAncestor, + origElement, + left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + origElement = element; + + while (element && element.parentNode && !firstFixedAncestor) { + + element = element.parentNode; + + if (element !== fabric.document && + fabric.util.getElementStyle(element, 'position') === 'fixed') { + firstFixedAncestor = element; + } + + if (element !== fabric.document && + origElement !== upperCanvasEl && + fabric.util.getElementStyle(element, 'position') === 'absolute') { + left = 0; + top = 0; + } + else if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + } + + return { left: left, top: top }; + } /** * Returns offset for a given element @@ -5423,10 +3840,11 @@ fabric.util.string = { * @return {Object} Object with "left" and "top" properties */ function getElementOffset(element) { - var docElem, win, + var docElem, box = {left: 0, top: 0}, doc = element && element.ownerDocument, offset = {left: 0, top: 0}, + scrollLeftTop, offsetAttributes = { 'borderLeftWidth': 'left', 'borderTopWidth': 'top', @@ -5446,14 +3864,12 @@ fabric.util.string = { if ( typeof element.getBoundingClientRect !== "undefined" ) { box = element.getBoundingClientRect(); } - if(doc != null && doc === doc.window){ - win = doc; - } else { - win = doc.nodeType === 9 && (doc.defaultView || doc.parentWindow); - } + + scrollLeftTop = fabric.util.getScrollLeftTop(element, null); + return { - left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + offset.left, - top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0) + offset.top + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top }; } @@ -5572,6 +3988,7 @@ fabric.util.string = { fabric.util.makeElement = makeElement; fabric.util.addClass = addClass; fabric.util.wrapElement = wrapElement; + fabric.util.getScrollLeftTop = getScrollLeftTop; fabric.util.getElementOffset = getElementOffset; fabric.util.getElementStyle = getElementStyle; @@ -5651,6 +4068,55 @@ fabric.util.string = { })(); +/** + * Wrapper around `console.log` (when available) + * @param {Any} values Values to log + */ +fabric.log = function() { }; + +<<<<<<< HEAD + fabric.util.removeFromArray = removeFromArray; + fabric.util.degreesToRadians = degreesToRadians; + fabric.util.radiansToDegrees = radiansToDegrees; + fabric.util.rotatePoint = rotatePoint; + fabric.util.transformPoint = transformPoint; + fabric.util.invertTransform = invertTransform; + fabric.util.toFixed = toFixed; + fabric.util.getRandomInt = getRandomInt; + fabric.util.falseFunction = falseFunction; + fabric.util.getKlass = getKlass; + fabric.util.resolveNamespace = resolveNamespace; + fabric.util.loadImage = loadImage; + fabric.util.enlivenObjects = enlivenObjects; + fabric.util.groupSVGElements = groupSVGElements; + fabric.util.populateWithProperties = populateWithProperties; + fabric.util.drawDashedLine = drawDashedLine; + fabric.util.createCanvasElement = createCanvasElement; + fabric.util.createImage = createImage; + fabric.util.createAccessors = createAccessors; + fabric.util.clipContext = clipContext; + fabric.util.multiplyTransformMatrices = multiplyTransformMatrices; + fabric.util.getFunctionBody = getFunctionBody; + fabric.util.drawArc = drawArc; +======= +/** + * Wrapper around `console.warn` (when available) + * @param {Any} Values to log as a warning + */ +fabric.warn = function() { }; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f + +if (typeof console !== 'undefined') { + ['log', 'warn'].forEach(function(methodName) { + if (typeof console[methodName] !== 'undefined' && console[methodName].apply) { + fabric[methodName] = function() { + return console[methodName].apply(console, arguments); + }; + } + }); +} + + (function() { /** @@ -5667,34 +4133,37 @@ fabric.util.string = { */ function animate(options) { - options || (options = { }); + requestAnimFrame(function(timestamp) { + options || (options = { }); - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || endValue - startValue; + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; - options.onStart && options.onStart(); + options.onStart && options.onStart(); + + (function tick(ticktime) { + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start); + if (abort()) { + options.onComplete && options.onComplete(); + return; + } + onChange(easing(currentTime, startValue, byValue, duration)); + if (time > finish) { + options.onComplete && options.onComplete(); + return; + } + requestAnimFrame(tick); + })(start); + }); - (function tick() { - time = +new Date(); - var currentTime = time > finish ? duration : (time - start); - if (abort()) { - options.onComplete && options.onComplete(); - return; - } - onChange(easing(currentTime, startValue, byValue, duration)); - if (time > finish) { - options.onComplete && options.onComplete(); - return; - } - requestAnimFrame(tick); - })(); } var _requestAnimFrame = fabric.window.requestAnimationFrame || @@ -5707,6 +4176,7 @@ fabric.util.string = { }; /** * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method * @memberOf fabric.util * @param {Function} callback Callback to invoke * @param {DOMElement} element optional Element to associate with animation @@ -5723,38 +4193,16 @@ fabric.util.string = { (function() { - /** - * Quadratic easing in - * @memberOf fabric.util.ease - */ - function easeInQuad(t, b, c, d) { - return c*(t/=d)*t + b; + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return { a: a, c: c, p: p, s: s }; } - /** - * Quadratic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuad(t, b, c, d) { - return -c *(t/=d)*(t-2) + b; - } - - /** - * Quadratic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuad(t, b, c, d) { - t /= (d/2); - if (t < 1) return c/2*t*t + b; - return -c/2 * ((--t)*(t-2) - 1) + b; - } - - /** - * Cubic easing in - * @memberOf fabric.util.ease - */ - function easeInCubic(t, b, c, d) { - return c*(t/=d)*t*t + b; + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); } /** @@ -5915,9 +4363,8 @@ fabric.util.string = { t /= d; if (t===1) return b+c; if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; } /** @@ -5930,9 +4377,8 @@ fabric.util.string = { t /= d; if (t===1) return b+c; if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + var opts = normalize(a, c, p, s); + return opts.a*Math.pow(2,-10*t) * Math.sin( (t*d-opts.s)*(2*Math.PI)/opts.p ) + opts.c + b; } /** @@ -5945,10 +4391,9 @@ fabric.util.string = { t /= d/2; if (t===2) return b+c; if (!p) p=d*(0.3*1.5); - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; + var opts = normalize(a, c, p, s); + if (t < 1) return -0.5 * elastic(opts, t, d) + b; + return opts.a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-opts.s)*(2*Math.PI)/opts.p )*0.5 + opts.c + b; } /** @@ -6019,10 +4464,41 @@ fabric.util.string = { * @namespace fabric.util.ease */ fabric.util.ease = { - easeInQuad: easeInQuad, - easeOutQuad: easeOutQuad, - easeInOutQuad: easeInOutQuad, - easeInCubic: easeInCubic, + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c*(t/=d)*t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d/2); + if (t < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c*(t/=d)*t*t + b; + }, + easeOutCubic: easeOutCubic, easeInOutCubic: easeInOutCubic, easeInQuart: easeInQuart, @@ -6070,13 +4546,6 @@ fabric.util.string = { toFixed = fabric.util.toFixed, multiplyTransformMatrices = fabric.util.multiplyTransformMatrices; - fabric.SHARED_ATTRIBUTES = [ - "transform", - "fill", "fill-opacity", "fill-rule", - "opacity", - "stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width" - ]; - var attributesMap = { 'fill-opacity': 'fillOpacity', 'fill-rule': 'fillRule', @@ -6161,55 +4630,13 @@ fabric.util.string = { return attributes; } - /** - * Returns an object of attributes' name/value, given element and an array of attribute names; - * Parses parent "g" nodes recursively upwards. - * @static - * @memberOf fabric - * @param {DOMElement} element Element to parse - * @param {Array} attributes Array of attributes to parse - * @return {Object} object containing parsed attributes' names/values - */ - function parseAttributes(element, attributes) { - - if (!element) { - return; - } - - var value, - parentAttributes = { }; - - // if there's a parent container (`g` node), parse its attributes recursively upwards - if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { - parentAttributes = fabric.parseAttributes(element.parentNode, attributes); - } - - var ownAttributes = attributes.reduce(function(memo, attr) { - value = element.getAttribute(attr); - if (value) { - attr = normalizeAttr(attr); - value = normalizeValue(attr, value, parentAttributes); - - memo[attr] = value; - } - return memo; - }, { }); - - // add values parsed from style, which take precedence over attributes - // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) - - ownAttributes = extend(ownAttributes, - extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); - return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); - } - /** * Parses "transform" attribute, returning an array of values * @static * @function * @memberOf fabric - * @param attributeValue {String} string containing attribute value - * @return {Array} array of 6 elements representing transformation matrix + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { @@ -6256,13 +4683,22 @@ fabric.util.string = { // == begin transform regexp number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)', + comma_wsp = '(?:\\s+,?\\s*|,\\s*)', skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', - rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))', - scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', - translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + ')' + + comma_wsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', matrix = '(?:(matrix)\\s*\\(\\s*' + '(' + number + ')' + comma_wsp + @@ -6348,49 +4784,6 @@ fabric.util.string = { }; })(); - /** - * Parses "points" attribute, returning an array of values - * @static - * @memberOf fabric - * @param points {String} points attribute string - * @return {Array} array of points - */ - function parsePointsAttribute(points) { - - // points attribute is required and must not be empty - if (!points) return null; - - points = points.trim(); - var asPairs = points.indexOf(',') > -1; - - points = points.split(/\s+/); - var parsedPoints = [ ], i, len; - - // points could look like "10,20 30,40" or "10 20 30 40" - if (asPairs) { - i = 0; - len = points.length; - for (; i < len; i++) { - var pair = points[i].split(','); - parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); - } - } - else { - i = 0; - len = points.length; - for (; i < len; i+=2) { - parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); - } - } - - // odd number of points is an error - if (parsedPoints.length % 2 !== 0) { - // return null; - } - - return parsedPoints; - } - function parseFontDeclaration(value, oStyle) { // TODO: support non-px font size @@ -6424,166 +4817,43 @@ fabric.util.string = { } /** - * Parses "style" attribute, retuning an object with values - * @static - * @memberOf fabric - * @param {SVGElement} element Element to parse - * @return {Object} Objects with values parsed from style attribute of an element + * @private */ - function parseStyleAttribute(element) { - var oStyle = { }, - style = element.getAttribute('style'), - attr, value; + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); - if (!style) return oStyle; + attr = normalizeAttr(pair[0].trim().toLowerCase()); + value = normalizeValue(attr, pair[1].trim()); - if (typeof style === 'string') { - style.replace(/;$/, '').split(';').forEach(function (chunk) { - var pair = chunk.split(':'); - - attr = normalizeAttr(pair[0].trim().toLowerCase()); - value = normalizeValue(attr, pair[1].trim()); - - if (attr === 'font') { - parseFontDeclaration(value, oStyle); - } - else { - oStyle[attr] = value; - } - }); - } - else { - for (var prop in style) { - if (typeof style[prop] === 'undefined') continue; - - attr = normalizeAttr(prop.toLowerCase()); - value = normalizeValue(attr, style[prop]); - - if (attr === 'font') { - parseFontDeclaration(value, oStyle); - } - else { - oStyle[attr] = value; - } - } - } - - return oStyle; - } - - function resolveGradients(instances) { - for (var i = instances.length; i--; ) { - var instanceFillValue = instances[i].get('fill'); - - if (/^url\(/.test(instanceFillValue)) { - - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - - if (fabric.gradientDefs[gradientId]) { - instances[i].set('fill', - fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); - } - } - } - } - - /** - * Transforms an array of svg elements to corresponding fabric.* instances - * @static - * @memberOf fabric - * @param {Array} elements Array of elements to parse - * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) - * @param {Object} [options] Options object - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function parseElements(elements, callback, options, reviver) { - var instances = new Array(elements.length), i = elements.length; - - function checkIfDone() { - if (--i === 0) { - instances = instances.filter(function(el) { - return el != null; - }); - resolveGradients(instances); - callback(instances); - } - } - - for (var index = 0, el, len = elements.length; index < len; index++) { - el = elements[index]; - var klass = fabric[capitalize(el.tagName)]; - if (klass && klass.fromElement) { - try { - if (klass.async) { - klass.fromElement(el, (function(index, el) { - return function(obj) { - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - }; - })(index, el), options); - } - else { - var obj = klass.fromElement(el, options); - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - } - } - catch(err) { - fabric.log(err); - } + if (attr === 'font') { + parseFontDeclaration(value, oStyle); } else { - checkIfDone(); + oStyle[attr] = value; } - } + }); } /** - * Returns CSS rules for a given SVG document - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} CSS rules of this document + * @private */ - function getCSSRules(doc) { - var styles = doc.getElementsByTagName('style'), - allRules = { }, - rules; + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') continue; - // very crude parsing of style contents - for (var i = 0, len = styles.length; i < len; i++) { - var styleContents = styles[0].textContent; + attr = normalizeAttr(prop.toLowerCase()); + value = normalizeValue(attr, style[prop]); - // remove comments - styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); - - rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); - rules = rules.map(function(rule) { return rule.trim(); }); - - rules.forEach(function(rule) { - var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); - rule = match[1]; - var declaration = match[2].trim(), - propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); - - if (!allRules[rule]) { - allRules[rule] = { }; - } - - for (var i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(/\s*:\s*/), - property = pair[0], - value = pair[1]; - - allRules[rule][property] = value; - } - }); + if (attr === 'font') { + parseFontDeclaration(value, oStyle); + } + else { + oStyle[attr] = value; + } } - - return allRules; } /** @@ -6696,7 +4966,7 @@ fabric.util.string = { }; fabric.gradientDefs = fabric.getGradientDefs(doc); - fabric.cssRules = getCSSRules(doc); + fabric.cssRules = fabric.getCSSRules(doc); // Precedence of rules: style > class > attribute @@ -6740,53 +5010,6 @@ fabric.util.string = { } }; - /** - * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) - * @memberof fabric - * @param {String} url - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromURL(url, callback, reviver) { - - url = url.replace(/^\n\s*/, '').trim(); - - svgCache.has(url, function (hasUrl) { - if (hasUrl) { - svgCache.get(url, function (value) { - var enlivedRecord = _enlivenCachedObject(value); - callback(enlivedRecord.objects, enlivedRecord.options); - }); - } - else { - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - } - }); - - function onComplete(r) { - - var xml = r.responseXML; - if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { - xml = new ActiveXObject('Microsoft.XMLDOM'); - xml.async = 'false'; - //IE chokes on DOCTYPE - xml.loadXML(r.responseText.replace(//i,'')); - } - if (!xml.documentElement) return; - - fabric.parseSVGDocument(xml.documentElement, function (results, options) { - svgCache.set(url, { - objects: fabric.util.array.invoke(results, 'toObject'), - options: options - }); - callback(results, options); - }, reviver); - } - } - /** * @private */ @@ -6803,139 +5026,435 @@ fabric.util.string = { } /** - * Takes string corresponding to an SVG document, and parses it into a set of fabric objects - * @memberof fabric - * @param {String} string - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromString(string, callback, reviver) { - string = string.trim(); - var doc; - if (typeof DOMParser !== 'undefined') { - var parser = new DOMParser(); - if (parser && parser.parseFromString) { - doc = parser.parseFromString(string, 'text/xml'); - } - } - else if (fabric.window.ActiveXObject) { - doc = new ActiveXObject('Microsoft.XMLDOM'); - doc.async = 'false'; - //IE chokes on DOCTYPE - doc.loadXML(string.replace(//i,'')); - } - - fabric.parseSVGDocument(doc.documentElement, function (results, options) { - callback(results, options); - }, reviver); - } - - /** - * Creates markup containing SVG font faces - * @param {Array} objects Array of fabric objects - * @return {String} + * @private */ - function createSVGFontFacesMarkup(objects) { - var markup = ''; - - for (var i = 0, len = objects.length; i < len; i++) { - if (objects[i].type !== 'text' || !objects[i].path) continue; - - markup += [ - '@font-face {', - 'font-family: ', objects[i].fontFamily, '; ', - 'src: url(\'', objects[i].path, '\')', - '}' - ].join(''); - } - - if (markup) { - markup = [ - '' - ].join(''); - } - - return markup; - } - - /** - * Creates markup containing SVG referenced elements like patterns, gradients etc. - * @param {fabric.Canvas} canvas instance of fabric.Canvas - * @return {String} - */ - function createSVGRefElementsMarkup(canvas) { - var markup = ''; - - if (canvas.backgroundColor && canvas.backgroundColor.source) { - markup = [ - '', '' - ].join(''); + ); } - - return markup; - } - - /** - * Parses an SVG document, returning all of the gradient declarations found in it - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element - */ - function getGradientDefs(doc) { - var linearGradientEls = doc.getElementsByTagName('linearGradient'), - radialGradientEls = doc.getElementsByTagName('radialGradient'), - el, i, - gradientDefs = { }; - - i = linearGradientEls.length; - for (; i--; ) { - el = linearGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - i = radialGradientEls.length; - for (; i--; ) { - el = radialGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - return gradientDefs; } extend(fabric, { - parseAttributes: parseAttributes, - parseElements: parseElements, - parseStyleAttribute: parseStyleAttribute, - parsePointsAttribute: parsePointsAttribute, - getCSSRules: getCSSRules, + /** + * Initializes gradients on instances, according to gradients parsed from a document + * @param {Array} instances + */ + resolveGradients: function(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); - loadSVGFromURL: loadSVGFromURL, - loadSVGFromString: loadSVGFromString, + if (!(/^url\(/).test(instanceFillValue)) continue; - createSVGFontFacesMarkup: createSVGFontFacesMarkup, - createSVGRefElementsMarkup: createSVGRefElementsMarkup, + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - getGradientDefs: getGradientDefs + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, i, + gradientDefs = { }; + + i = linearGradientEls.length; + for (; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + i = radialGradientEls.length; + for (; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }; + + // if there's a parent container (`g` node), parse its attributes recursively upwards + if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { + attr = normalizeAttr(attr); + value = normalizeValue(attr, value, parentAttributes); + + memo[attr] = value; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + ownAttributes = extend(ownAttributes, + extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + + return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver) { + fabric.ElementsParser.parse(elements, callback, options, reviver); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) return oStyle; + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param points {String} points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ], i, len; + + // points could look like "10,20 30,40" or "10 20 30 40" + if (asPairs) { + i = 0; + len = points.length; + for (; i < len; i++) { + var pair = points[i].split(','); + parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); + } + } + else { + i = 0; + len = points.length; + for (; i < len; i+=2) { + parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, + rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); + rule = match[1]; + var declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberof fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + loadSVGFromURL: function(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i,'')); + } + if (!xml.documentElement) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberof fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + loadSVGFromString: function(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(//i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + }, + + /** + * Creates markup containing SVG font faces + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '' + ].join(''); + } + + return markup; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @param {fabric.Canvas} canvas instance of fabric.Canvas + * @return {String} + */ + createSVGRefElementsMarkup: function(canvas) { + var markup = [ ]; + + _createSVGPattern(markup, canvas, 'backgroundColor'); + _createSVGPattern(markup, canvas, 'overlayColor'); + + return markup.join(''); + } }); })(typeof exports !== 'undefined' ? exports : this); +fabric.ElementsParser = { + + parse: function(elements, callback, options, reviver) { + + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + + this.instances = new Array(elements.length); + this.numElements = elements.length; + + this.createObjects(); + }, + + createObjects: function() { + for (var i = 0, len = this.elements.length; i < len; i++) { + this.createObject(this.elements[i], i); + } + }, + + createObject: function(el, index) { + var klass = fabric[fabric.util.string.capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + this._createObject(klass, el, index); + } + catch(err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }, + + _createObject: function(klass, el, index) { + if (klass.async) { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + else { + var obj = klass.fromElement(el, this.options); + this.reviver && this.reviver(el, obj); + this.instances.splice(index, 0, obj); + this.checkIfDone(); + } + }, + + createCallback: function(index, el) { + var _this = this; + return function(obj) { + _this.reviver && _this.reviver(el, obj); + _this.instances.splice(index, 0, obj); + _this.checkIfDone(); + }; + }, + + checkIfDone: function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + return el != null; + }); + fabric.resolveGradients(this.instances); + this.callback(this.instances); + } + } +}; + + (function(global) { "use strict"; @@ -7227,167 +5746,167 @@ fabric.util.string = { })(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Intersection) { - fabric.warn('fabric.Intersection is already defined'); - return; - } - - /** - * Intersection class - * @class fabric.Intersection - * @memberOf fabric - * @constructor - */ - function Intersection(status) { - this.status = status; - this.points = []; - } - - fabric.Intersection = Intersection; - - fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { - - /** - * Appends a point to intersection - * @param {fabric.Point} point - */ - appendPoint: function (point) { - this.points.push(point); - }, - - /** - * Appends points to intersection - * @param {Array} points - */ - appendPoints: function (points) { - this.points = this.points.concat(points); - } - }; - - /** - * Checks if one line intersects another - * @static - * @param {fabric.Point} a1 - * @param {fabric.Point} a2 - * @param {fabric.Point} b1 - * @param {fabric.Point} b2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { - var result, - ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), - ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), - u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); - if (u_b !== 0) { - var ua = ua_t / u_b, - ub = ub_t / u_b; - if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { - result = new Intersection("Intersection"); - result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); - } - else { - result = new Intersection(); - } - } - else { - if (ua_t === 0 || ub_t === 0) { - result = new Intersection("Coincident"); - } - else { - result = new Intersection("Parallel"); - } - } - return result; - }; - - /** - * Checks if line intersects polygon - * @static - * @param {fabric.Point} a1 - * @param {fabric.Point} a2 - * @param {Array} points - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ - var result = new Intersection(), - length = points.length; - - for (var i = 0; i < length; i++) { - var b1 = points[i], - b2 = points[(i+1) % length], - inter = Intersection.intersectLineLine(a1, a2, b1, b2); - - result.appendPoints(inter.points); - } - if (result.points.length > 0) { - result.status = "Intersection"; - } - return result; - }; - - /** - * Checks if polygon intersects another polygon - * @static - * @param {Array} points1 - * @param {Array} points2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { - var result = new Intersection(), - length = points1.length; - - for (var i = 0; i < length; i++) { - var a1 = points1[i], - a2 = points1[(i+1) % length], - inter = Intersection.intersectLinePolygon(a1, a2, points2); - - result.appendPoints(inter.points); - } - if (result.points.length > 0) { - result.status = "Intersection"; - } - return result; - }; - - /** - * Checks if polygon intersects rectangle - * @static - * @param {Array} points - * @param {Number} r1 - * @param {Number} r2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { - var min = r1.min(r2), - max = r1.max(r2), - topRight = new fabric.Point(max.x, min.y), - bottomLeft = new fabric.Point(min.x, max.y), - inter1 = Intersection.intersectLinePolygon(min, topRight, points), - inter2 = Intersection.intersectLinePolygon(topRight, max, points), - inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), - inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), - result = new Intersection(); - - result.appendPoints(inter1.points); - result.appendPoints(inter2.points); - result.appendPoints(inter3.points); - result.appendPoints(inter4.points); - - if (result.points.length > 0) { - result.status = "Intersection"; - } - return result; - }; - -})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + /** + * Appends a point to intersection + * @param {fabric.Point} point + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * Appends points to intersection + * @param {Array} points + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * Checks if one line intersects another + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (u_b !== 0) { + var ua = ua_t / u_b, + ub = ub_t / u_b; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection("Intersection"); + result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (ua_t === 0 || ub_t === 0) { + result = new Intersection("Coincident"); + } + else { + result = new Intersection("Parallel"); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ + var result = new Intersection(), + length = points.length; + + for (var i = 0; i < length; i++) { + var b1 = points[i], + b2 = points[(i+1) % length], + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length; + + for (var i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i+1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {Number} r1 + * @param {Number} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); (function(global) { @@ -7908,12 +6427,33 @@ fabric.util.string = { opacity: isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity) }; } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } /* _FROM_SVG_END_ */ /** * Gradient class * @class fabric.Gradient * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition */ fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { @@ -8060,6 +6600,169 @@ fabric.util.string = { this.coords.x1, this.coords.y1, this.coords.r1, this.coords.x2, this.coords.y2, this.coords.r2); } +<<<<<<< HEAD +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + /** + * Appends a point to intersection + * @param {fabric.Point} point + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * Appends points to intersection + * @param {Array} points + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * Checks if one line intersects another + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (u_b !== 0) { + var ua = ua_t / u_b, + ub = ub_t / u_b; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection("Intersection"); + result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (ua_t === 0 || ub_t === 0) { + result = new Intersection("Coincident"); + } + else { + result = new Intersection("Parallel"); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ + var result = new Intersection(), + length = points.length; + + for (var i = 0; i < length; i++) { + var b1 = points[i], + b2 = points[(i+1) % length], + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length; + + for (var i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i+1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {Number} r1 + * @param {Number} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); +======= for (var i = 0, len = this.colorStops.length; i < len; i++) { var color = this.colorStops[i].color, opacity = this.colorStops[i].opacity, @@ -8130,22 +6833,10 @@ fabric.util.string = { coords = { }; if (type === 'linear') { - coords = { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; + coords = getLinearCoords(el); } else if (type === 'radial') { - coords = { - x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', - y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', - r1: 0, - x2: el.getAttribute('cx') || '50%', - y2: el.getAttribute('cy') || '50%', - r2: el.getAttribute('r') || '50%' - }; + coords = getRadialCoords(el); } for (var i = colorStopEls.length; i--; ) { @@ -8191,13 +6882,17 @@ fabric.util.string = { options[prop] = fabric.util.toFixed(object.height * percents / 100, 2); } } - // normalize rendering point (should be from top/left corner rather than center of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] -= fabric.util.toFixed(object.width / 2, 2); - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] -= fabric.util.toFixed(object.height / 2, 2); - } + normalize(options, prop, object); + } + } + + // normalize rendering point (should be from top/left corner rather than center of the shape) + function normalize(options, prop, object) { + if (prop === 'x1' || prop === 'x2') { + options[prop] -= fabric.util.toFixed(object.width / 2, 2); + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= fabric.util.toFixed(object.height / 2, 2); } } @@ -8207,13 +6902,9 @@ fabric.util.string = { */ function _convertValuesToPercentUnits(object, options) { for (var prop in options) { - // normalize rendering point (should be from center rather than top/left corner of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] += fabric.util.toFixed(object.width / 2, 2); - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] += fabric.util.toFixed(object.height / 2, 2); - } + + normalize(options, prop, object); + // convert to percent units if (prop === 'x1' || prop === 'x2' || prop === 'r2') { options[prop] = fabric.util.toFixed(options[prop] / object.width * 100, 2) + '%'; @@ -8233,6 +6924,7 @@ fabric.util.string = { * @class fabric.Pattern * @see {@link http://fabricjs.com/patterns/|Pattern demo} * @see {@link http://fabricjs.com/dynamic-patterns/|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition */ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { @@ -8363,9 +7055,15 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ toLive: function(ctx) { var source = typeof this.source === 'function' ? this.source() : this.source; + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) return ''; + if (source.naturalWidth === 0 || source.naturalHeight === 0) return ''; + } return ctx.createPattern(source, this.repeat); } }); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f (function(global) { @@ -8383,6 +7081,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * Shadow class * @class fabric.Shadow * @see {@link http://fabricjs.com/shadows/|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition */ fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { @@ -8420,6 +7119,13 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ affectStroke: false, + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + /** * Constructor * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)") @@ -8497,12 +7203,28 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * @return {Object} Object representation of a shadow instance */ toObject: function() { - return { - color: this.color, - blur: this.blur, - offsetX: this.offsetX, - offsetY: this.offsetY - }; + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + if (this.color !== proto.color) { + obj.color = this.color; + } + if (this.blur !== proto.blur) { + obj.blur = this.blur; + } + if (this.offsetX !== proto.offsetX) { + obj.offsetX = this.offsetX; + } + if (this.offsetY !== proto.offsetY) { + obj.offsetY = this.offsetY; + } + return obj; } }); @@ -8530,7 +7252,6 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ var extend = fabric.util.object.extend, getElementOffset = fabric.util.getElementOffset, removeFromArray = fabric.util.removeFromArray, - removeListener = fabric.util.removeListener, CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); @@ -8540,6 +7261,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * @mixes fabric.Collection * @mixes fabric.Observable * @see {@link http://fabricjs.com/static_canvas/|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition * @fires before:render * @fires after:render * @fires canvas:cleared @@ -8562,56 +7284,43 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ }, /** - * Background color of canvas instance - * @type String + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} * @default */ backgroundColor: '', /** - * Background image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setBackgroundImage} - * @type String + * Background image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. + * Backwards incompatibility note: The "backgroundImageOpacity" + * and "backgroundImageStretch" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. + * @type fabric.Image * @default */ - backgroundImage: '', + backgroundImage: null, /** - * Opacity of the background image of the canvas instance - * @type Float + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} * @default */ - backgroundImageOpacity: 1, + overlayColor: '', /** - * Indicates whether the background image should be stretched to fit the - * dimensions of the canvas instance. - * @type Boolean + * Overlay image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. + * Backwards incompatibility note: The "overlayImageLeft" + * and "overlayImageTop" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#left} and {@link fabric.Image#top}. + * @type fabric.Image * @default */ - backgroundImageStretch: true, - - /** - * Overlay image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setOverlayImage} - * @type String - * @default - */ - overlayImage: '', - - /** - * Left offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageLeft: 0, - - /** - * Top offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageTop: 0, + overlayImage: null, /** * Indicates whether toObject/toDatalessObject should include default values @@ -8658,13 +7367,6 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ allowTouchScrolling: false, - /** - * The transformation (in the format of Canvas transform) which focuses the viewport - * @type Array - * @default - */ - viewportTransform: [1, 0, 0, 1, 0, 0], - /** * Callback; invoked right before object is about to be scaled/rotated * @param {fabric.Object} target Object that's about to be scaled/rotated @@ -8693,6 +7395,9 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ if (options.backgroundColor) { this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, this.renderAll.bind(this)); + } this.calcOffset(); }, @@ -8709,74 +7414,115 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {String} url url of an image to set overlay to + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the overlay image - * @param {Number} [options.overlayImageLeft] {@link fabric.StaticCanvas#overlayImageLeft|Left offset} of overlay image - * @param {Number} [options.overlayImageTop] {@link fabric.StaticCanvas#overlayImageTop|Top offset} of overlay image + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Displaced overlayImage (left and top != 0) + * @example Normal overlayImage with left/top = 0 * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * overlayImageLeft: 100, - * overlayImageTop: 100 + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setOverlayImage: function (url, callback, options) { // TODO (kangax): test callback - fabric.util.loadImage(url, function(img) { - this.overlayImage = img; - if (options && ('overlayImageLeft' in options)) { - this.overlayImageLeft = options.overlayImageLeft; - } - if (options && ('overlayImageTop' in options)) { - this.overlayImageTop = options.overlayImageTop; - } - callback && callback(); - }, this); - - return this; + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); }, /** * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {String} url URL of an image to set background to - * @param {Function} callback callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the background image - * @param {Float} [options.backgroundImageOpacity] {@link fabric.StaticCanvas#backgroundImageOpacity|Opacity} of the background image of the canvas instance - * @param {Boolean} [options.backgroundImageStretch] Indicates whether the background image should be {@link fabric.StaticCanvas#backgroundImageStretch|strechted} to fit the canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} - * @example Normal backgroundImage - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Stretched backgroundImage with opacity - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * backgroundImageOpacity: 0.5, - * backgroundImageStretch: true + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setBackgroundImage: function (url, callback, options) { - fabric.util.loadImage(url, function(img) { - this.backgroundImage = img; - if (options && ('backgroundImageOpacity' in options)) { - this.backgroundImageOpacity = options.backgroundImageOpacity; - } - if (options && ('backgroundImageStretch' in options)) { - this.backgroundImageStretch = options.backgroundImageStretch; - } - callback && callback(); - }, this); + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, - return this; + /** + * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); }, /** * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {String|fabric.Pattern} backgroundColor Color or pattern to set background color to - * @param {Function} callback callback to invoke when background color is set + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} @@ -8786,20 +7532,63 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * canvas.setBackgroundColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png' * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); */ setBackgroundColor: function(backgroundColor, callback) { - if (backgroundColor.source) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img) { + this[property] = new fabric.Image(img, options); + callback && callback(); + }, this); + } + else { + this[property] = image; + callback && callback(); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String)} color Object with pattern information or color value + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + if (color.source) { var _this = this; - fabric.util.loadImage(backgroundColor.source, function(img) { - _this.backgroundColor = new fabric.Pattern({ + fabric.util.loadImage(color.source, function(img) { + _this[property] = new fabric.Pattern({ source: img, - repeat: backgroundColor.repeat + repeat: color.repeat, + offsetX: color.offsetX, + offsetY: color.offsetY }); callback && callback(); }); } else { - this.backgroundColor = backgroundColor; + this[property] = color; callback && callback(); } @@ -8953,73 +7742,6 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, - /** - * Returns canvas zoom level - * @return {Number} - */ - getZoom: function () { - return sqrt(this.viewportTransform[0] * this.viewportTransform[3]); - }, - - /** - * Returns point at center of viewport - * @return {fabric.Point} the top left corner of the viewport - */ - getViewportCenter: function () { - var wh = fabric.util.transformPoint( - new fabric.Point(this.getWidth(), this.getHeight()), - this.viewportTransform - ), - x = this.viewportTransform[4], - y = this.viewportTransform[5]; - - return new fabric.Point(this.getWidth()/2 + x, this.getHeight()/2 + y); - }, - - /** - * Sets zoom level of this canvas instance - * @param {Number} value to set zoom to, less than 1 zooms out - * @return {fabric.Canvas} instance - * @chainable true - */ - setZoom: function (value) { - // TODO: just change the scale, preserve other transformations - this.viewportTransform[0] = value; - this.viewportTransform[3] = value; - this.renderAll(); - for (var i = 0, len = this._objects.length; i < len; i++) { - this._objects[i].setCoords(); - } - return this; - }, - - /** - * Centers viewport of this canvas instance on given point - * @param {Numer} x value for center of viewport - * @param {Numer} y value for center of viewport - * @return {fabric.Canvas} instance - * @chainable true - */ - setViewportCenter: function (x, y) { - var wh = fabric.util.transformPoint( - new fabric.Point(this.getWidth(), this.getHeight()), - this.viewportTransform - ); - this.viewportTransform[4] = x - wh.x/2; - this.viewportTransform[5] = y - wh.y/2; - this.renderAll(); - return this; - }, - - /** - * Centers viewport of this canvas instance - * @return {fabric.Canvas} instance - * @chainable true - */ - centerViewport: function () { - return this.setViewportCenter(this.getWidth()/2, this.getHeight()/2); - }, - /** * Returns <canvas> element corresponding to this instance * @return {HTMLCanvasElement} @@ -9071,14 +7793,8 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ _onObjectAdded: function(obj) { this.stateful && obj.setupState(); - obj.canvas = this; obj.setCoords(); - if (obj._objects) { - for (var i = 0, len = obj._objects.length; i < len; i++) { - obj._objects[i].canvas = this; - obj._objects[i].setCoords(); - } - } + obj.canvas = this; this.fire('object:added', { target: obj }); obj.fire('added'); }, @@ -9088,18 +7804,17 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (this.getActiveObject() === obj) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared'); + } + this.fire('object:removed', { target: obj }); obj.fire('removed'); }, - /** - * Returns an array of objects this instance has - * @return {Array} - */ - getObjects: function () { - return this._objects; - }, - /** * Clears specified context of canvas element * @param {CanvasRenderingContext2D} ctx Context to clear @@ -9150,6 +7865,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ renderAll: function (allOnTop) { var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + var activeGroup = this.getActiveGroup(); if (this.contextTop && this.selection && !this._groupSelector) { this.clearContext(this.contextTop); @@ -9165,50 +7881,15 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ fabric.util.clipContext(this, canvasToDrawOn); } - if (this.backgroundColor) { - canvasToDrawOn.fillStyle = this.backgroundColor.toLive - ? this.backgroundColor.toLive(canvasToDrawOn) - : this.backgroundColor; - - canvasToDrawOn.fillRect( - this.backgroundColor.offsetX || 0, - this.backgroundColor.offsetY || 0, - this.width, - this.height); - } - - if (typeof this.backgroundImage === 'object') { - this._drawBackroundImage(canvasToDrawOn); - } - - var activeGroup = this.getActiveGroup(); - for (var i = 0, length = this._objects.length; i < length; ++i) { - if (!activeGroup || - (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { - this._draw(canvasToDrawOn, this._objects[i]); - } - } - - // delegate rendering to group selection (if one exists) - if (activeGroup) { - //Store objects in group preserving order, then replace - var sortedObjects = []; - this.forEachObject(function (object) { - if (activeGroup.contains(object)) { - sortedObjects.push(object); - } - }); - activeGroup._set('objects', sortedObjects); - this._draw(canvasToDrawOn, activeGroup); - } + this._renderBackground(canvasToDrawOn); + this._renderObjects(canvasToDrawOn, activeGroup); + this._renderActiveGroup(canvasToDrawOn, activeGroup); if (this.clipTo) { canvasToDrawOn.restore(); } - if (this.overlayImage) { - canvasToDrawOn.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop); - } + this._renderOverlay(canvasToDrawOn); if (this.controlsAboveOverlay && this.interactive) { this.drawControls(canvasToDrawOn); @@ -9221,19 +7902,80 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * @private - * @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup */ - _drawBackroundImage: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + _renderObjects: function(ctx, activeGroup) { + for (var i = 0, length = this._objects.length; i < length; ++i) { + if (!activeGroup || + (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { + this._draw(ctx, this._objects[i]); + } + } + }, - if (this.backgroundImageStretch) { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderActiveGroup: function(ctx, activeGroup) { + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + + //Store objects in group preserving order, then replace + var sortedObjects = []; + this.forEachObject(function (object) { + if (activeGroup.contains(object)) { + sortedObjects.push(object); + } + }); + activeGroup._set('objects', sortedObjects); + this._draw(ctx, activeGroup); } - else { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (this.backgroundColor) { + ctx.fillStyle = this.backgroundColor.toLive + ? this.backgroundColor.toLive(ctx) + : this.backgroundColor; + + ctx.fillRect( + this.backgroundColor.offsetX || 0, + this.backgroundColor.offsetY || 0, + this.width, + this.height); + } + if (this.backgroundImage) { + this.backgroundImage.render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + if (this.overlayColor) { + ctx.fillStyle = this.overlayColor.toLive + ? this.overlayColor.toLive(ctx) + : this.overlayColor; + + ctx.fillRect( + this.overlayColor.offsetX || 0, + this.overlayColor.offsetY || 0, + this.width, + this.height); + } + if (this.overlayImage) { + this.overlayImage.render(ctx); } - canvasToDrawOn.restore(); }, /** @@ -9282,11 +8024,11 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Centers object horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - object.set('left', this.getCenter().left); + this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); this.renderAll(); return this; }, @@ -9294,12 +8036,12 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Centers object vertically. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center vertically * @return {fabric.Canvas} thisArg * @chainable */ centerObjectV: function (object) { - object.set('top', this.getCenter().top); + this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); this.renderAll(); return this; }, @@ -9307,12 +8049,28 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ /** * Centers object vertically and horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ - centerObject: function (object) { - return this.centerObjectH(object).centerObjectV(object); + centerObject: function(object) { + var center = this.getCenter(); + + this._centerObject(object, new fabric.Point(center.left, center.top)); + this.renderAll(); + return this; + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + return this; }, /** @@ -9351,42 +8109,75 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ if (activeGroup) { this.discardActiveGroup(); } + var data = { - objects: this.getObjects().map(function (instance) { - // TODO (kangax): figure out how to clean this up - var originalValue; - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } - var object = instance[methodName](propertiesToInclude); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, this), - background: (this.backgroundColor && this.backgroundColor.toObject) - ? this.backgroundColor.toObject() - : this.backgroundColor + objects: this._toObjects(methodName, propertiesToInclude) }; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.src; - data.backgroundImageOpacity = this.backgroundImageOpacity; - data.backgroundImageStretch = this.backgroundImageStretch; - } - if (this.overlayImage) { - data.overlayImage = this.overlayImage.src; - data.overlayImageLeft = this.overlayImageLeft; - data.overlayImageTop = this.overlayImageTop; - } + + extend(data, this.__serializeBgOverlay()); + fabric.util.populateWithProperties(this, data, propertiesToInclude); + if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); - activeGroup.forEachObject(function(o) { o.set('active', true) }); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); } return data; }, + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this.getObjects().map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function() { + var data = { + background: (this.backgroundColor && this.backgroundColor.toObject) + ? this.backgroundColor.toObject() + : this.backgroundColor + }; + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject() + : this.overlayColor; + } + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.toObject(); + } + if (this.overlayImage) { + data.overlayImage = this.overlayImage.toObject(); + } + + return data; + }, + /* _TO_SVG_START_ */ /** * Returns SVG representation of canvas @@ -9425,8 +8216,29 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ toSVG: function(options, reviver) { options || (options = { }); + var markup = []; + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + + this._setSVGBgOverlayColor(markup, 'backgroundColor'); + this._setSVGBgOverlayImage(markup, 'backgroundImage'); + + this._setSVGObjects(markup, reviver); + + this._setSVGBgOverlayColor(markup, 'overlayColor'); + this._setSVGBgOverlayImage(markup, 'overlayImage'); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { if (!options.suppressPreamble) { markup.push( '', @@ -9434,53 +8246,42 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' ); } + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { markup.push( - '', - 'Created with Fabric.js ', fabric.version, '', - '', fabric.createSVGFontFacesMarkup(this.getObjects()), fabric.createSVGRefElementsMarkup(this), '' + '', + 'Created with Fabric.js ', fabric.version, '', + '', + fabric.createSVGFontFacesMarkup(this.getObjects()), + fabric.createSVGRefElementsMarkup(this), + '' ); + }, - if (this.backgroundColor && this.backgroundColor.source) { - markup.push( - '' - ); - } - - if (this.backgroundImage) { - markup.push( - '' - ); - } - - if (this.overlayImage) { - markup.push( - '' - ); - } - + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { var activeGroup = this.getActiveGroup(); if (activeGroup) { this.discardActiveGroup(); @@ -9490,30 +8291,52 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ } if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); - activeGroup.forEachObject(function(o) { o.set('active', true) }); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); } - markup.push(''); - - return markup.join(''); }, - /* _TO_SVG_END_ */ /** - * Removes an object from canvas and returns it - * @param {fabric.Object} object Object to remove - * @return {fabric.Object} removed object + * @private */ - remove: function (object) { - // removing active object should fire "selection:cleared" events - if (this.getActiveObject() === object) { - this.fire('before:selection:cleared', { target: object }); - this.discardActiveObject(); - this.fire('selection:cleared'); + _setSVGBgOverlayImage: function(markup, property) { + if (this[property] && this[property].toSVG) { + markup.push(this[property].toSVG()); } - - return fabric.Collection.remove.call(this, object); }, + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + if (this[property] && this[property].source) { + markup.push( + '' + ); + } + else if (this[property] && property === 'overlayColor') { + markup.push( + '' + ); + } + }, + /* _TO_SVG_END_ */ + /** * Moves an object to the bottom of the stack of drawn objects * @param {fabric.Object} object Object to send to back @@ -9539,38 +8362,30 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ }, /** +<<<<<<< HEAD + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: [1, 0, 0, 1, 0, 0], + + /** + * Callback; invoked right before object is about to be scaled/rotated + * @param {fabric.Object} target Object that's about to be scaled/rotated +======= * Moves an object down in stack of drawn objects * @param {fabric.Object} object Object to send * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object * @return {fabric.Canvas} thisArg * @chainable +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f */ sendBackwards: function (object, intersecting) { var idx = this._objects.indexOf(object); // if object is not on the bottom of stack if (idx !== 0) { - var newIdx; - - if (intersecting) { - newIdx = idx; - - // traverse down the stack looking for the nearest intersecting object - for (var i=idx-1; i>=0; --i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx-1; - } + var newIdx = this._findNewLowerIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); @@ -9579,6 +8394,35 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (var i=idx-1; i>=0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + /** * Moves an object up in stack of drawn objects * @param {fabric.Object} object Object to send @@ -9591,27 +8435,7 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ // if object is not on top of stack (last item in an array) if (idx !== this._objects.length-1) { - var newIdx; - - if (intersecting) { - newIdx = idx; - - // traverse up the stack looking for the nearest intersecting object - for (var i = idx + 1; i < this._objects.length; ++i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx+1; - } + var newIdx = this._findNewUpperIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); @@ -9620,6 +8444,35 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ return this; }, + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (var i = idx + 1; i < this._objects.length; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx+1; + } + + return newIdx; + }, + /** * Moves an object to specified level in stack of drawn objects * @param {fabric.Object} object Object to send @@ -9634,27 +8487,13 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ }, /** - * Clears a canvas element and removes all event handlers. + * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { this.clear(); - - if (!this.interactive) return this; - - if (fabric.isTouchSupported) { - removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - if (typeof Event !== 'undefined' && 'remove' in Event) { - Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); - } - } - else { - removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(fabric.window, 'resize', this._onResize); - } + this.interactive && this.removeListeners(); return this; }, @@ -9768,6 +8607,82 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ width: 1, +<<<<<<< HEAD + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return sqrt(this.viewportTransform[0] * this.viewportTransform[3]); + }, + + /** + * Returns point at center of viewport + * @return {fabric.Point} the top left corner of the viewport + */ + getViewportCenter: function () { + var wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform + ), + x = this.viewportTransform[4], + y = this.viewportTransform[5]; + + return new fabric.Point(this.getWidth()/2 + x, this.getHeight()/2 + y); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + // TODO: just change the scale, preserve other transformations + this.viewportTransform[0] = value; + this.viewportTransform[3] = value; + this.renderAll(); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].setCoords(); + } + return this; + }, + + /** + * Centers viewport of this canvas instance on given point + * @param {Numer} x value for center of viewport + * @param {Numer} y value for center of viewport + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportCenter: function (x, y) { + var wh = fabric.util.transformPoint( + new fabric.Point(this.getWidth(), this.getHeight()), + this.viewportTransform + ); + this.viewportTransform[4] = x - wh.x/2; + this.viewportTransform[5] = y - wh.y/2; + this.renderAll(); + return this; + }, + + /** + * Centers viewport of this canvas instance + * @return {fabric.Canvas} instance + * @chainable true + */ + centerViewport: function () { + return this.setViewportCenter(this.getWidth()/2, this.getHeight()/2); + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, +======= /** * Shadow object representing shadow of this shape. * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), @@ -9776,6 +8691,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * @default */ shadow: null, +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f /** * Line endings style of a brush (one of "butt", "round", "square") @@ -9881,12 +8797,27 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * Inovoked on mouse move * @param {Object} pointer */ +<<<<<<< HEAD + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj.canvas = this; + obj.setCoords(); + if (obj._objects) { + for (var i = 0, len = obj._objects.length; i < len; i++) { + obj._objects[i].canvas = this; + obj._objects[i].setCoords(); + } + } + this.fire('object:added', { target: obj }); + obj.fire('added'); +======= onMouseMove: function(pointer) { this._captureDrawingPath(pointer); // redraw curve // clear top canvas this.canvas.clearContext(this.canvas.contextTop); this._render(); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f }, /** @@ -9953,7 +8884,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype var p1 = this._points[0]; var p2 = this._points[1]; - + //if we only have 2 points in the path and they are the same //it means that the user only clicked the canvas without moving the mouse //then we should be drawing a dot. A path isn't drawn between two identical dots @@ -9986,10 +8917,6 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * @private */ _getSVGPathData: function() { - var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); - for (var i = 0, len = this._points.length; i < len; i++) { - this._points[i] = fabric.util.transformPoint(this._points[i], ivt); - } this.box = this.getPathBoundingBox(this._points); return this.convertPointsToSVGPath( this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy); @@ -10105,7 +9032,12 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false); var path = this.createPath(pathData); - path.set({ left: originLeft, top: originTop }); + path.set({ + left: originLeft, + top: originTop, + originX: 'center', + originY: 'center' + }); this.canvas.add(path); path.setCoords(); @@ -10191,6 +9123,8 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric radius: point.radius, left: point.x, top: point.y, + originX: 'center', + originY: 'center', fill: point.fill }); @@ -10198,8 +9132,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } - var group = new fabric.Group(circles); - group.canvas = this.canvas; + var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -10333,6 +9266,8 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric height: sprayChunk[j].width, left: sprayChunk[j].x + 1, top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', fill: this.color }); @@ -10345,9 +9280,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects); - group.canvas = this.canvas; - + var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -10383,13 +9316,9 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric var ctx = this.canvas.contextTop; ctx.fillStyle = this.color; ctx.save(); - var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { var point = this.sprayChunkPoints[i]; - var tpoint = fabric.util.transformPoint({x: point.x, y: point.y}, ivt); - point.x = tpoint.x; - point.y = tpoint.y; if (typeof point.opacity !== 'undefined') { ctx.globalAlpha = point.opacity; } @@ -10405,7 +9334,6 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric this.sprayChunkPoints = [ ]; var x, y, width, radius = this.width / 2; - var vpt = this.canvas.viewportTransform; for (var i = 0; i < this.density; i++) { @@ -10421,17 +9349,32 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric else { width = this.dotWidth; } - - var point = new fabric.Point(x, y); - point = fabric.util.transformPoint(point, vpt); - point.width = width + + var point = { x: x, y: y, width: width }; if (this.randomOpacity) { point.opacity = fabric.util.getRandomInt(0, 100) / 100; } +<<<<<<< HEAD + /** + * Return an SVG path based on our captured points and their bounding box + * + * @private + */ + _getSVGPathData: function() { + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); + for (var i = 0, len = this._points.length; i < len; i++) { + this._points[i] = fabric.util.transformPoint(this._points[i], ivt); + } + this.box = this.getPathBoundingBox(this._points); + return this.convertPointsToSVGPath( + this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy); + }, +======= this.sprayChunkPoints.push(point); } +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f this.sprayChunks.push(this.sprayChunkPoints); } @@ -10495,15 +9438,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }); +<<<<<<< HEAD + circles.push(circle); + } + var group = new fabric.Group(circles); + group.canvas = this.canvas; +======= (function() { +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f var getPointer = fabric.util.getPointer, degreesToRadians = fabric.util.degreesToRadians, radiansToDegrees = fabric.util.radiansToDegrees, atan2 = Math.atan2, abs = Math.abs, - min = Math.min, - max = Math.max, STROKE_OFFSET = 0.5; @@ -10512,6 +9460,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @class fabric.Canvas * @extends fabric.StaticCanvas * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition * * @fires object:modified * @fires object:rotating @@ -10685,12 +9634,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._groupSelector = null; this._initWrapperElement(); this._createUpperCanvas(); - this._initEvents(); + this._initEventListeners(); this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); +<<<<<<< HEAD + var group = new fabric.Group(rects); + group.canvas = this.canvas; + + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); +======= this.calcOffset(); }, +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f /** * Resets the current transform to its original values and chooses the type of resizing based on the event @@ -10733,11 +9690,53 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab t.originY = 'center'; } } +<<<<<<< HEAD + } + var uniqueRectsArray = [ ]; + for (key in uniqueRects) { + uniqueRectsArray.push(uniqueRects[key]); + } + + return uniqueRectsArray; + }, + + /** + * Renders brush + */ + render: function() { + var ctx = this.canvas.contextTop; + ctx.fillStyle = this.color; + ctx.save(); + var ivt = fabric.util.invertTransform(this.canvas.viewportTransform); + + for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { + var point = this.sprayChunkPoints[i]; + var tpoint = fabric.util.transformPoint({x: point.x, y: point.y}, ivt); + point.x = tpoint.x; + point.y = tpoint.y; + if (typeof point.opacity !== 'undefined') { + ctx.globalAlpha = point.opacity; + } + ctx.fillRect(point.x, point.y, point.width, point.width); + } + ctx.restore(); + }, + + /** + * @param {Object} pointer + */ + addSprayChunk: function(pointer) { + this.sprayChunkPoints = [ ]; + + var x, y, width, radius = this.width / 2; + var vpt = this.canvas.viewportTransform; +======= else { t.originX = t.original.originX; t.originY = t.original.originY; } }, +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f /** * Checks if point is contained within an area of given object @@ -10746,7 +9745,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target) { - var pointer = this.getPointer(e, true), + var pointer = this.getPointer(e), xy = this._normalizePointer(target, pointer); // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html @@ -10754,9 +9753,311 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return (target.containsPoint(xy) || target._findTargetCorner(e, this._offset)); }, +<<<<<<< HEAD + if (this.dotWidthVariance) { + width = fabric.util.getRandomInt( + // bottom clamp width to 1 + Math.max(1, this.dotWidth - this.dotWidthVariance), + this.dotWidth + this.dotWidthVariance); + } + else { + width = this.dotWidth; + } + + var point = new fabric.Point(x, y); + point = fabric.util.transformPoint(point, vpt); + point.width = width +======= /** * @private */ + _normalizePointer: function (object, pointer) { + var activeGroup = this.getActiveGroup(), + x = pointer.x, + y = pointer.y; + + var isObjectInGroup = ( + activeGroup && + object.type !== 'group' && + activeGroup.contains(object) + ); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f + + if (isObjectInGroup) { + x -= activeGroup.left; + y -= activeGroup.top; + } + return { x: x, y: y }; + }, + + /** + * Returns true if object is transparent at a certain location + * @param {fabric.Object} target Object to check + * @param {Number} x Left coordinate + * @param {Number} y Top coordinate + * @return {Boolean} + */ + isTargetTransparent: function (target, x, y) { + var hasBorders = target.hasBorders, + transparentCorners = target.transparentCorners; + + target.hasBorders = target.transparentCorners = false; + + this._draw(this.contextCache, target); + + target.hasBorders = hasBorders; + target.transparentCorners = transparentCorners; + + var isTransparent = fabric.util.isTransparent( + this.contextCache, x, y, this.targetFindTolerance); + + this.clearContext(this.contextCache); + + return isTransparent; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldClearSelection: function (e, target) { + var activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(); + + return ( + !target + || + (target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e.shiftKey) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldCenterTransform: function (e, target) { + if (!target) return; + + var t = this._currentTransform, + centerTransform; + + if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') { + centerTransform = this.centeredScaling || target.centeredScaling; + } + else if (t.action === 'rotate') { + centerTransform = this.centeredRotation || target.centeredRotation; + } + + return centerTransform ? !e.altKey : e.altKey; + }, + + /** + * @private + */ + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; + + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } + + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + + return origin; + }, + + /** + * @private + */ + _getActionFromCorner: function(target, corner) { + var action = 'drag'; + if (corner) { + action = (corner === 'ml' || corner === 'mr') + ? 'scaleX' + : (corner === 'mt' || corner === 'mb') + ? 'scaleY' + : corner === 'mtr' + ? 'rotate' + : 'scale'; + } + return action; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target) { + if (!target) return; + + var corner = target._findTargetCorner(e, this._offset), + pointer = getPointer(e, target.canvas.upperCanvasEl), + action = this._getActionFromCorner(target, corner), + origin = this._getOriginFromCorner(target, corner); + + this._currentTransform = { + target: target, + action: action, + scaleX: target.scaleX, + scaleY: target.scaleY, + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + left: target.left, + top: target.top, + theta: degreesToRadians(target.angle), + width: target.width * target.scaleX, + mouseXSign: 1, + mouseYSign: 1 + }; + + this._currentTransform.original = { + left: target.left, + top: target.top, + scaleX: target.scaleX, + scaleY: target.scaleY, + originX: origin.x, + originY: origin.y + }; + + this._resetCurrentTransform(e); + }, + + /** + * Translates object by "setting" its left/top + * @private + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + */ + _translateObject: function (x, y) { + var target = this._currentTransform.target; + + if (!target.get('lockMovementX')) { + target.set('left', x - this._currentTransform.offsetX); + } + if (!target.get('lockMovementY')) { + target.set('top', y - this._currentTransform.offsetY); + } + }, + + /** + * Scales object by invoking its scaleX/scaleY methods + * @private + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + */ + _scaleObject: function (x, y, by) { + var t = this._currentTransform, + offset = this._offset, + target = t.target, + lockScalingX = target.get('lockScalingX'), + lockScalingY = target.get('lockScalingY'); + + if (lockScalingX && lockScalingY) return; + + // Get the constraint point + var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + var localMouse = target.toLocalPoint(new fabric.Point(x - offset.left, y - offset.top), t.originX, t.originY); + + this._setLocalMouse(localMouse, t); + + // Actually scale the object + this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by); + + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + }, + + /** + * @private + */ + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by) { + var target = transform.target; + + transform.newScaleX = target.scaleX; + transform.newScaleY = target.scaleY; + + if (by === 'equally' && !lockScalingX && !lockScalingY) { + this._scaleObjectEqually(localMouse, target, transform); + } + else if (!by) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + + lockScalingX || target.set('scaleX', transform.newScaleX); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + else if (by === 'x' && !target.get('lockUniScaling')) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + lockScalingX || target.set('scaleX', transform.newScaleX); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + + this._flipObject(transform); + }, + + /** + * @private + */ +<<<<<<< HEAD + containsPoint: function (e, target) { + var pointer = this.getPointer(e, true), + xy = this._normalizePointer(target, pointer); +======= + _scaleObjectEqually: function(localMouse, target, transform) { +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f + + var dist = localMouse.y + localMouse.x; + + var lastDist = (target.height + (target.strokeWidth)) * transform.original.scaleY + + (target.width + (target.strokeWidth)) * transform.original.scaleX; + + // We use transform.scaleX/Y instead of target.scaleX/Y + // because the object may have a min scale and we'll loose the proportions + transform.newScaleX = transform.original.scaleX * dist / lastDist; + transform.newScaleY = transform.original.scaleY * dist / lastDist; + + target.set('scaleX', transform.newScaleX); + target.set('scaleY', transform.newScaleY); + }, + + /** + * @private + */ +<<<<<<< HEAD _normalizePointer: function (object, pointer) { var activeGroup = this.getActiveGroup(), x = pointer.x, @@ -10774,71 +10075,91 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab lt = fabric.util.transformPoint(lt, this.viewportTransform, true); x -= lt.x; y -= lt.y; - } - return { x: x, y: y }; - }, - - /** - * Returns true if object is transparent at a certain location - * @param {fabric.Object} target Object to check - * @param {Number} x Left coordinate - * @param {Number} y Top coordinate - * @return {Boolean} - */ - isTargetTransparent: function (target, x, y) { - var cacheContext = this.contextCache; - - var hasBorders = target.hasBorders, - transparentCorners = target.transparentCorners; - - target.hasBorders = target.transparentCorners = false; - - this._draw(cacheContext, target); - - target.hasBorders = hasBorders; - target.transparentCorners = transparentCorners; - - // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 - if (this.targetFindTolerance > 0) { - if (x > this.targetFindTolerance) { - x -= this.targetFindTolerance; +======= + _flipObject: function(transform) { + if (transform.newScaleX < 0) { + if (transform.originX === 'left') { + transform.originX = 'right'; } - else { - x = 0; - } - if (y > this.targetFindTolerance) { - y -= this.targetFindTolerance; - } - else { - y = 0; + else if (transform.originX === 'right') { + transform.originX = 'left'; } } - var isTransparent = true; - var imageData = cacheContext.getImageData( - x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (var i = 3, l = imageData.data.length; i < l; i += 4) { - var temp = imageData.data[i]; - isTransparent = temp <= 0; - if (isTransparent === false) break; //Stop if colour found + if (transform.newScaleY < 0) { + if (transform.originY === 'top') { + transform.originY = 'bottom'; + } + else if (transform.originY === 'bottom') { + transform.originY = 'top'; + } +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } - - imageData = null; - this.clearContext(cacheContext); - - return isTransparent; }, /** * @private - * @param {Event} e Event object - * @param {fabric.Object} target */ - _shouldClearSelection: function (e, target) { - var activeGroup = this.getActiveGroup(); + _setLocalMouse: function(localMouse, t) { + var target = t.target; + if (t.originX === 'right') { + localMouse.x *= -1; + } + else if (t.originX === 'center') { + localMouse.x *= t.mouseXSign * 2; + + if (localMouse.x < 0) { + t.mouseXSign = -t.mouseXSign; + } + } + + if (t.originY === 'bottom') { + localMouse.y *= -1; + } + else if (t.originY === 'center') { + localMouse.y *= t.mouseYSign * 2; + + if (localMouse.y < 0) { + t.mouseYSign = -t.mouseYSign; + } + } + + // adjust the mouse coordinates when dealing with padding + if (abs(localMouse.x) > target.padding) { + if (localMouse.x < 0) { + localMouse.x += target.padding; + } + else { + localMouse.x -= target.padding; + } + } + else { // mouse is within the padding, set to 0 + localMouse.x = 0; + } + + if (abs(localMouse.y) > target.padding) { + if (localMouse.y < 0) { + localMouse.y += target.padding; + } + else { + localMouse.y -= target.padding; + } + } + else { + localMouse.y = 0; + } + }, + + /** + * Rotates object by invoking its rotate method + * @private + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + */ + _rotateObject: function (x, y) { + +<<<<<<< HEAD return ( !target || ( target && @@ -11159,6 +10480,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _rotateObject: function (x, y) { +======= +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f var t = this._currentTransform, o = this._offset; @@ -11244,6 +10567,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private */ +<<<<<<< HEAD _findSelectedObjects: function (e) { var group = [ ], x1 = this._groupSelector.ex, @@ -11287,6 +10611,15 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.fire('selection:created', { target: group }); this.renderAll(); } +======= + _isLastRenderedObject: function(e) { + return ( + this.controlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay.visible && + this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && + this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f }, /** @@ -11297,6 +10630,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab findTarget: function (e, skipGroup) { if (this.skipTargetFind) return; +<<<<<<< HEAD var target, pointer = this.getPointer(e, true); @@ -11307,21 +10641,32 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)) { target = this.lastRenderedObjectWithControlsAboveOverlay; return target; +======= + if (this._isLastRenderedObject(e)) { + return this.lastRenderedObjectWithControlsAboveOverlay; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } // first check current group (if one exists) var activeGroup = this.getActiveGroup(); if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { - target = activeGroup; - return target; + return activeGroup; } - // then check all of the objects on canvas + return this._searchPossibleTargets(e); + }, + + /** + * @private + */ + _searchPossibleTargets: function(e) { + // Cache all targets where their bounding box contains point. - var possibleTargets = []; + var possibleTargets = [], + target, + pointer = this.getPointer(e); for (var i = this._objects.length; i--; ) { - if (this._objects[i] && this._objects[i].visible && this._objects[i].evented && @@ -11337,6 +10682,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } } + for (var j = 0, len = possibleTargets.length; j < len; j++) { pointer = this.getPointer(e, true); var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); @@ -11464,6 +10810,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return this.upperCanvasEl; }, + /** + * @private + * @param {Object} object + */ + _setActiveObject: function(object) { + if (this._activeObject) { + this._activeObject.set('active', false); + } + this._activeObject = object; + object.set('active', true); + }, + /** * Sets given object as the only active object on canvas * @param {fabric.Object} object Object to set as an active one @@ -11472,14 +10830,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ setActiveObject: function (object, e) { - if (this._activeObject) { - this._activeObject.set('active', false); - } - this._activeObject = object; - object.set('active', true); - + this._setActiveObject(object); this.renderAll(); - this.fire('object:selected', { target: object, e: e }); object.fire('selected', { e: e }); return this; @@ -11494,29 +10846,50 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Discards currently active object - * @return {fabric.Canvas} thisArg - * @chainable + * @private */ - discardActiveObject: function () { + _discardActiveObject: function() { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = null; + }, + + /** + * Discards currently active object + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + this._discardActiveObject(); + this.renderAll(); + this.fire('selection:cleared', { e: e }); return this; }, + /** + * @private + * @param {fabric.Group} group + */ + _setActiveGroup: function(group) { + this._activeGroup = group; + if (group) { + group.canvas = this; + group.set('active', true); + } + }, + /** * Sets active group to a speicified one * @param {fabric.Group} group Group to set as a current one * @return {fabric.Canvas} thisArg * @chainable */ - setActiveGroup: function (group) { - this._activeGroup = group; + setActiveGroup: function (group, e) { + this._setActiveGroup(group); if (group) { - group.canvas = this; - group.set('active', true); + this.fire('object:selected', { target: group, e: e }); + group.fire('selected', { e: e }); } return this; }, @@ -11530,15 +10903,24 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Removes currently active group - * @return {fabric.Canvas} thisArg + * @private */ - discardActiveGroup: function () { + _discardActiveGroup: function() { var g = this.getActiveGroup(); if (g) { g.destroy(); } - return this.setActiveGroup(null); + this.setActiveGroup(null); + }, + + /** + * Discards currently active group + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function (e) { + this._discardActiveGroup(); + this.fire('selection:cleared', { e: e }); + return this; }, /** @@ -11552,8 +10934,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab for ( ; i < len; i++) { allObjects[i].set('active', false); } - this.discardActiveGroup(); - this.discardActiveObject(); + this._discardActiveGroup(); + this._discardActiveObject(); return this; }, @@ -11561,14 +10943,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * Deactivates all objects and dispatches appropriate events * @return {fabric.Canvas} thisArg */ - deactivateAllWithDispatch: function () { + deactivateAllWithDispatch: function (e) { var activeObject = this.getActiveGroup() || this.getActiveObject(); if (activeObject) { - this.fire('before:selection:cleared', { target: activeObject }); + this.fire('before:selection:cleared', { target: activeObject, e: e }); } this.deactivateAll(); if (activeObject) { - this.fire('selection:cleared'); + this.fire('selection:cleared', { e: e }); } return this; }, @@ -11580,23 +10962,39 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab drawControls: function(ctx) { var activeGroup = this.getActiveGroup(); if (activeGroup) { - ctx.save(); - fabric.Group.prototype.transform.call(activeGroup, ctx); - activeGroup.drawBorders(ctx).drawControls(ctx); - ctx.restore(); + this._drawGroupControls(ctx, activeGroup); } else { - for (var i = 0, len = this._objects.length; i < len; ++i) { - if (!this._objects[i] || !this._objects[i].active) continue; - - ctx.save(); - fabric.Object.prototype.transform.call(this._objects[i], ctx); - this._objects[i].drawBorders(ctx).drawControls(ctx); - ctx.restore(); - - this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; - } + this._drawObjectsControls(ctx); } + }, + + /** + * @private + */ + _drawGroupControls: function(ctx, activeGroup) { + this._drawControls(ctx, activeGroup, 'Group'); + }, + + /** + * @private + */ + _drawObjectsControls: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i] || !this._objects[i].active) continue; + this._drawControls(ctx, this._objects[i], 'Object'); + this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; + } + }, + + /** + * @private + */ + _drawControls: function(ctx, object, klass) { + ctx.save(); + fabric[klass].prototype.transform.call(object, ctx); + object.drawBorders(ctx).drawControls(ctx); + ctx.restore(); } }); @@ -11626,14 +11024,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab (function(){ var cursorMap = [ - 'n-resize', - 'ne-resize', - 'e-resize', - 'se-resize', - 's-resize', - 'sw-resize', - 'w-resize', - 'nw-resize' + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + 'nw-resize' ], cursorOffset = { 'mt': 0, // n @@ -11655,34 +11053,110 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * Adds mouse listeners to canvas * @private */ - _initEvents: function () { - var _this = this; + _initEventListeners: function () { + this._bindEvents(); + + addListener(fabric.window, 'resize', this._onResize); + + // mouse events + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); + + // touch events + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (typeof Event !== 'undefined' && 'add' in Event) { + Event.add(this.upperCanvasEl, 'gesture', this._onGesture); + Event.add(this.upperCanvasEl, 'drag', this._onDrag); + Event.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.add(this.upperCanvasEl, 'shake', this._onShake); + } + }, + + /** + * @private + */ + _bindEvents: function() { this._onMouseDown = this._onMouseDown.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseUp = this._onMouseUp.bind(this); this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + }, - this._onGesture = function(e, s) { - _this.__onTransformGesture(e, s); - }; + /** + * Removes all event listeners + */ + removeListeners: function() { + removeListener(fabric.window, 'resize', this._onResize); - addListener(fabric.window, 'resize', this._onResize); + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); - if (fabric.isTouchSupported) { - addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - if (typeof Event !== 'undefined' && 'add' in Event) { - Event.add(this.upperCanvasEl, 'gesture', this._onGesture); - } - } - else { - addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + if (typeof Event !== 'undefined' && 'remove' in Event) { + Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); + Event.remove(this.upperCanvasEl, 'drag', this._onDrag); + Event.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.remove(this.upperCanvasEl, 'shake', this._onShake); } }, + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, s) { + this.__onTransformGesture && this.__onTransformGesture(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, s) { + this.__onDrag && this.__onDrag(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js wheel event + * @param {Event} [self] Inner Event object + */ + _onMouseWheel: function(e, s) { + this.__onMouseWheel && this.__onMouseWheel(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e,s) { + this.__onOrientationChange && this.__onOrientationChange(e,s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e,s) { + this.__onShake && this.__onShake(e,s); + }, + /** * @private * @param {Event} e Event object fired on mousedown @@ -11690,14 +11164,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _onMouseDown: function (e) { this.__onMouseDown(e); - !fabric.isTouchSupported && addListener(fabric.document, 'mouseup', this._onMouseUp); - fabric.isTouchSupported && addListener(fabric.document, 'touchend', this._onMouseUp); + addListener(fabric.document, 'mouseup', this._onMouseUp); + addListener(fabric.document, 'touchend', this._onMouseUp); - !fabric.isTouchSupported && addListener(fabric.document, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && addListener(fabric.document, 'touchmove', this._onMouseMove); + addListener(fabric.document, 'mousemove', this._onMouseMove); + addListener(fabric.document, 'touchmove', this._onMouseMove); - !fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); }, /** @@ -11707,14 +11181,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _onMouseUp: function (e) { this.__onMouseUp(e); - !fabric.isTouchSupported && removeListener(fabric.document, 'mouseup', this._onMouseUp); - fabric.isTouchSupported && removeListener(fabric.document, 'touchend', this._onMouseUp); + removeListener(fabric.document, 'mouseup', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onMouseUp); - !fabric.isTouchSupported && removeListener(fabric.document, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', this._onMouseMove); + removeListener(fabric.document, 'mousemove', this._onMouseMove); + removeListener(fabric.document, 'touchmove', this._onMouseMove); - !fabric.isTouchSupported && addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); }, /** @@ -11742,16 +11216,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _shouldRender: function(target, pointer) { var activeObject = this.getActiveGroup() || this.getActiveObject(); - return ( + return !!( (target && ( - target.isMoving || - target !== activeObject)) || - (!target && activeObject) || + target.isMoving || + target !== activeObject)) + || + (!target && !!activeObject) + || + (!target && !activeObject && !this._groupSelector) + || (pointer && - this._previousPointer && - this.selection && ( - pointer.x !== this._previousPointer.x || - pointer.y !== this._previousPointer.y)) + this._previousPointer && + this.selection && ( + pointer.x !== this._previousPointer.x || + pointer.y !== this._previousPointer.y)) ); }, @@ -11763,55 +11241,19 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mouseup */ __onMouseUp: function (e) { - var target, - pointer, - render; + var target; if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._isCurrentlyDrawing = false; - if (this.clipTo) { - this.contextTop.restore(); - } - this.freeDrawingBrush.onMouseUp(); - this.fire('mouse:up', { e: e }); + this._onMouseUpInDrawingMode(e); return; } if (this._currentTransform) { - - var transform = this._currentTransform; - - target = transform.target; - if (target._scaling) { - target._scaling = false; - } - - target.setCoords(); - - // only fire :modified event if target coordinates were changed during mousedown-mouseup - if (this.stateful && target.hasStateChanged()) { - this.fire('object:modified', { target: target }); - target.fire('modified'); - } - - if (this._previousOriginX && this._previousOriginY) { - - var originPoint = target.translateToOriginPoint( - target.getCenterPoint(), - this._previousOriginX, - this._previousOriginY); - - target.originX = this._previousOriginX; - target.originY = this._previousOriginY; - - target.left = originPoint.x; - target.top = originPoint.y; - - this._previousOriginX = null; - this._previousOriginY = null; - } + this._finalizeCurrentTransform(); + target = this._currentTransform.target; } else { +<<<<<<< HEAD pointer = this.getPointer(e, true); } @@ -11820,27 +11262,28 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.selection && this._groupSelector) { // group selection was completed, determine its bounds this._findSelectedObjects(e); +======= + target = this.findTarget(e, true); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords(); - activeGroup.isMoving = false; - this._setCursor(this.defaultCursor); - } + var shouldRender = this._shouldRender(target, this.getPointer(e)); - // clear selection and current transformation - this._groupSelector = null; - this._currentTransform = null; + this._maybeGroupObjects(e); if (target) { target.isMoving = false; } - render && this.renderAll(); + shouldRender && this.renderAll(); + this._handleCursorAndEvent(e, target); + }, + + _handleCursorAndEvent: function(e, target) { this._setCursorFromEvent(e, target); + // TODO: why are we doing this? var _this = this; setTimeout(function () { _this._setCursorFromEvent(e, target); @@ -11850,13 +11293,59 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab target && target.fire('mouseup', { e: e }); }, + /** + * @private + */ + _finalizeCurrentTransform: function() { + + var transform = this._currentTransform; + var target = transform.target; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + + this._restoreOriginXY(target); + }, + + /** + * @private + * @param {Object} target Object to restore + */ + _restoreOriginXY: function(target) { + if (this._previousOriginX && this._previousOriginY) { + + var originPoint = target.translateToOriginPoint( + target.getCenterPoint(), + this._previousOriginX, + this._previousOriginY); + + target.originX = this._previousOriginX; + target.originY = this._previousOriginY; + + target.left = originPoint.x; + target.top = originPoint.y; + + this._previousOriginX = null; + this._previousOriginY = null; + } + }, + /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDownInDrawingMode: function(e) { this._isCurrentlyDrawing = true; - this.discardActiveObject().renderAll(); + this.discardActiveObject(e).renderAll(); if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } @@ -11864,6 +11353,32 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.fire('mouse:down', { e: e }); }, + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseMove(pointer); + } + this.upperCanvasEl.style.cursor = this.freeDrawingCursor; + this.fire('mouse:move', { e: e }); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + this._isCurrentlyDrawing = false; + if (this.clipTo) { + this.contextTop.restore(); + } + this.freeDrawingBrush.onMouseUp(); + this.fire('mouse:up', { e: e }); + }, + /** * Method that defines the actions when mouse is clic ked on canvas. * The method inits the currentTransform parameters and renders all the @@ -11873,6 +11388,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousedown */ __onMouseDown: function (e) { + // accept only left clicks var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; if (!isLeftClick && !fabric.isTouchSupported) return; @@ -11886,53 +11402,77 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this._currentTransform) return; var target = this.findTarget(e), +<<<<<<< HEAD pointer = this.getPointer(e, true), corner, render; +======= + pointer = this.getPointer(e); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f // save pointer for check in __onMouseUp event this._previousPointer = pointer; - render = this._shouldRender(target, pointer); + var shouldRender = this._shouldRender(target, pointer), + shouldGroup = this._shouldGroup(e, target); if (this._shouldClearSelection(e, target)) { - if (this.selection) { - this._groupSelector = { - ex: pointer.x, - ey: pointer.y, - top: 0, - left: 0 - }; - } - this.deactivateAllWithDispatch(); - target && target.selectable && this.setActiveObject(target, e); + this._clearSelection(e, target, pointer); } - else if (this._shouldHandleGroupLogic(e, target)) { - this._handleGroupLogic(e, target); + else if (shouldGroup) { + this._handleGrouping(e, target); target = this.getActiveGroup(); } - else { - // determine if it's a drag or rotate case - this.stateful && target.saveState(); - - if ((corner = target._findTargetCorner(e, this._offset))) { - this.onBeforeScaleRotate(target); - } - - if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { - this.deactivateAll(); - this.setActiveObject(target, e); - } + if (target && target.selectable && !shouldGroup) { + this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } // we must renderAll so that active image is placed on the top canvas - render && this.renderAll(); + shouldRender && this.renderAll(); this.fire('mouse:down', { target: target, e: e }); target && target.fire('mousedown', { e: e }); }, + /** + * @private + */ + _beforeTransform: function(e, target) { + var corner; + + this.stateful && target.saveState(); + + // determine if it's a drag or rotate case + if ((corner = target._findTargetCorner(e, this._offset))) { + this.onBeforeScaleRotate(target); + } + + if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { + this.deactivateAll(); + this.setActiveObject(target, e); + } + }, + + /** + * @private + */ + _clearSelection: function(e, target, pointer) { + this.deactivateAllWithDispatch(e); + + if (target && target.selectable) { + this.setActiveObject(target, e); + } + else if (this.selection) { + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + } + }, + /** * @private * @param {Object} target Object for that origin is set to center @@ -11983,48 +11523,51 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousemove */ __onMouseMove: function (e) { + var target, pointer; if (this.isDrawingMode) { +<<<<<<< HEAD if (this._isCurrentlyDrawing) { this.freeDrawingBrush.onMouseMove(this.getPointer(e, true)); } this.upperCanvasEl.style.cursor = this.freeDrawingCursor; this.fire('mouse:move', { e: e }); +======= + this._onMouseMoveInDrawingMode(e); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f return; } var groupSelector = this._groupSelector; - // We initially clicked in an empty area, so we draw a box for multiple selection. + // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { pointer = this.getPointer(e, true); +<<<<<<< HEAD groupSelector.left = pointer.x - groupSelector.ex; groupSelector.top = pointer.y - groupSelector.ey; +======= + groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; + groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f this.renderTop(); } else if (!this._currentTransform) { - // alias style to elimintate unnecessary lookup - var style = this.upperCanvasEl.style; - - // Here we are hovering the canvas then we will determine - // what part of the pictures we are hovering to change the caret symbol. - // We won't do that while dragging or rotating in order to improve the - // performance. target = this.findTarget(e); if (!target || target && !target.selectable) { - // no target - set default cursor - style.cursor = this.defaultCursor; + this.upperCanvasEl.style.cursor = this.defaultCursor; } else { - // set proper cursor this._setCursorFromEvent(e, target); } } else { +<<<<<<< HEAD // object is being transformed (scaled/rotated/moved/etc.) pointer = fabric.util.transformPoint( getPointer(e, this.upperCanvasEl), @@ -12071,40 +11614,112 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (!reset && transform.currentAction === 'scale') { this._resetCurrentTransform(e, target); } - - transform.currentAction = 'scaleEqually'; - this._scaleObject(x, y, 'equally'); - } - - this.fire('object:scaling', { target: target, e: e }); - target.fire('scaling', { e: e }); - } - else if (transform.action === 'scaleX') { - this._scaleObject(x, y, 'x'); - - this.fire('object:scaling', { target: target, e: e}); - target.fire('scaling', { e: e }); - } - else if (transform.action === 'scaleY') { - this._scaleObject(x, y, 'y'); - - this.fire('object:scaling', { target: target, e: e}); - target.fire('scaling', { e: e }); - } - else { - this._translateObject(x, y); - - this.fire('object:moving', { target: target, e: e}); - target.fire('moving', { e: e }); - this._setCursor(this.moveCursor); - } - - this.renderAll(); +======= + this._transformObject(e); } +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f + this.fire('mouse:move', { target: target, e: e }); target && target.fire('mousemove', { e: e }); }, + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + + var pointer = getPointer(e, this.upperCanvasEl), + transform = this._currentTransform; + + transform.reset = false, + transform.target.isMoving = true; + + this._beforeScaleTransform(e, transform); + this._performTransformAction(e, transform, pointer); + + this.renderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + target = transform.target, + action = transform.action; + + if (action === 'rotate') { + this._rotateObject(x, y); + this._fire('rotating', target, e); + } + else if (action === 'scale') { + this._onScale(e, transform, x, y); + this._fire('scaling', target, e); + } + else if (action === 'scaleX') { + this._scaleObject(x, y, 'x'); + this._fire('scaling', target, e); + } + else if (action === 'scaleY') { + this._scaleObject(x, y, 'y'); + this._fire('scaling', target, e); + } + else { + this._translateObject(x, y); + this._fire('moving', target, e); + this._setCursor(this.moveCursor); + } + }, + + /** + * @private + */ + _fire: function(eventName, target, e) { + this.fire('object:' + eventName, { target: target, e: e}); + target.fire(eventName, { e: e }); + }, + + /** + * @private + */ + _beforeScaleTransform: function(e, transform) { + if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { + var centerTransform = this._shouldCenterTransform(e, transform.target); + + // Switch from a normal resize to center-based + if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || + // Switch from center-based resize to normal one + (!centerTransform && transform.originX === 'center' && transform.originY === 'center') + ) { + this._resetCurrentTransform(e); + transform.reset = true; + } + } + }, + + /** + * @private + */ + _onScale: function(e, transform, x, y) { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + if ((e.shiftKey || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { + transform.currentAction = 'scale'; + this._scaleObject(x, y); + } + else { + // Switch from a normal resize to proportional + if (!transform.reset && transform.currentAction === 'scale') { + this._resetCurrentTransform(e, transform.target); + } + + transform.currentAction = 'scaleEqually'; + this._scaleObject(x, y, 'equally'); + } + }, + /** * Sets the cursor depending on where the canvas is being hovered. * Note: very buggy in Opera @@ -12112,9 +11727,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Object} target Object that the mouse is hovering, if so. */ _setCursorFromEvent: function (e, target) { - var s = this.upperCanvasEl.style; - if (!target) { - s.cursor = this.defaultCursor; + var style = this.upperCanvasEl.style; + + if (!target || !target.selectable) { + style.cursor = this.defaultCursor; return false; } else { @@ -12125,34 +11741,250 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab && target._findTargetCorner(e, this._offset); if (!corner) { - s.cursor = target.hoverCursor || this.hoverCursor; + style.cursor = target.hoverCursor || this.hoverCursor; } else { - if (corner in cursorOffset) { - var n = Math.round((target.getAngle() % 360) / 45); - if (n<0) { - n += 8; // full circle ahead - } - n += cursorOffset[corner]; - // normalize n to be from 0 to 7 - n %= 8; - s.cursor = cursorMap[n]; - } - else if (corner === 'mtr' && target.hasRotatingPoint) { - s.cursor = this.rotationCursor; - } - else { - s.cursor = this.defaultCursor; - return false; - } + this._setCornerCursor(corner, target); } } return true; + }, + + /** + * @private + */ + _setCornerCursor: function(corner, target) { + var style = this.upperCanvasEl.style; + + if (corner in cursorOffset) { + style.cursor = this._getRotatedCornerCursor(corner, target); + } + else if (corner === 'mtr' && target.hasRotatingPoint) { + style.cursor = this.rotationCursor; + } + else { + style.cursor = this.defaultCursor; + return false; + } + }, + + /** + * @private + */ + _getRotatedCornerCursor: function(corner, target) { + var n = Math.round((target.getAngle() % 360) / 45); + + if (n < 0) { + n += 8; // full circle ahead + } + n += cursorOffset[corner]; + // normalize n to be from 0 to 7 + n %= 8; + + return cursorMap[n]; } }); })(); +(function(){ + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this.getActiveObject(); + return e.shiftKey && + (this.getActiveGroup() || (activeObject && activeObject !== target)) + && this.selection; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + + if (target === this.getActiveGroup()) { + + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + if (this.getActiveGroup()) { + this._updateActiveGroup(target, e); + } + else { + this._createActiveGroup(target, e); + } + + if (this._activeGroup) { + this._activeGroup.saveCoords(); + } + }, + + /** + * @private + */ + _updateActiveGroup: function(target, e) { + var activeGroup = this.getActiveGroup(); + + if (activeGroup.contains(target)) { + + activeGroup.removeWithUpdate(target); + this._resetObjectTransform(activeGroup); + target.set('active', false); + + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(e); + // activate last remaining object + this.setActiveObject(activeGroup.item(0)); + return; + } + } + else { + activeGroup.addWithUpdate(target); + this._resetObjectTransform(activeGroup); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.set('active', true); + }, + + /** + * @private + */ + _createActiveGroup: function(target, e) { + + if (this._activeObject && target !== this._activeObject) { + + var group = this._createGroup(target); + + this.setActiveGroup(group); + this._activeObject = null; + + this.fire('selection:created', { target: group, e: e }); + } + + target.set('active', true); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + + var objects = this.getObjects(); + + var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); + + var groupObjects = isActiveLower + ? [ this._activeObject, target ] + : [ target, this._activeObject ]; + + return new fabric.Group(groupObjects, { + originX: 'center', + originY: 'center' + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(); + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + group = new fabric.Group(group.reverse(), { + originX: 'center', + originY: 'center' + }); + this.setActiveGroup(group, e); + group.saveCoords(); + this.fire('selection:created', { target: group }); + this.renderAll(); + } + }, + + /** + * @private + */ + _collectObjects: function() { + var group = [ ], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + isClick = x1 === x2 && y1 === y2; + + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) continue; + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || + currentObject.containsPoint(selectionX1Y1) || + currentObject.containsPoint(selectionX2Y2) + ) { + currentObject.set('active', true); + group.push(currentObject); + + // only add one object if it's a click + if (isClick) break; + } + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords().setCoords(); + activeGroup.isMoving = false; + this._setCursor(this.defaultCursor); + } + + // clear selection and current transformation + this._groupSelector = null; + this._currentTransform = null; + } + }); + +})(); + + fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** @@ -12301,15 +12133,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.deactivateAll(); } + this.renderAll(true); + + var data = this.__toDataURL(format, quality, cropping); + // restoring width, height for `renderAll` to draw // background properly (while context is scaled) this.width = origWidth; this.height = origHeight; - this.renderAll(true); - - var data = this.__toDataURL(format, quality, cropping); - ctx.scale(1 / multiplier, 1 / multiplier); this.setWidth(origWidth).setHeight(origHeight); @@ -12425,72 +12257,74 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var _this = this; this._enlivenObjects(serialized.objects, function () { - _this._setBgOverlayImages(serialized, callback); + _this._setBgOverlay(serialized, callback); }, reviver); return this; }, - _setBgOverlayImages: function(serialized, callback) { - + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { var _this = this, - backgroundPatternLoaded, - backgroundImageLoaded, - overlayImageLoaded; + loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } var cbIfLoaded = function () { - callback && backgroundImageLoaded && overlayImageLoaded && backgroundPatternLoaded && callback(); + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + _this.renderAll(); + callback && callback(); + } }; - if (serialized.backgroundImage) { - this.setBackgroundImage(serialized.backgroundImage, function() { + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); - _this.backgroundImageOpacity = serialized.backgroundImageOpacity; - _this.backgroundImageStretch = serialized.backgroundImageStretch; + cbIfLoaded(); + }, - _this.renderAll(); + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; - backgroundImageLoaded = true; + if (!value) { + loaded[property] = true; + return; + } - cbIfLoaded(); + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.Image.fromObject(value, function(img) { + _this[property] = img; + loaded[property] = true; + callback && callback(); }); } else { - backgroundImageLoaded = true; - } - - if (serialized.overlayImage) { - this.setOverlayImage(serialized.overlayImage, function() { - - _this.overlayImageLeft = serialized.overlayImageLeft || 0; - _this.overlayImageTop = serialized.overlayImageTop || 0; - - _this.renderAll(); - overlayImageLoaded = true; - - cbIfLoaded(); + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); }); } - else { - overlayImageLoaded = true; - } - - if (serialized.background) { - this.setBackgroundColor(serialized.background, function() { - - _this.renderAll(); - backgroundPatternLoaded = true; - - cbIfLoaded(); - }); - } - else { - backgroundPatternLoaded = true; - } - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) { - callback && callback(); - } }, /** @@ -12545,9 +12379,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Clones canvas instance * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children */ - clone: function (callback) { - var data = JSON.stringify(this); + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); this.cloneWithoutData(function(clone) { clone.loadFromJSON(data, function() { callback && callback(clone); @@ -12680,6 +12515,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Root object class from which all 2d shape classes inherit from * @class fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#objects} + * @see {@link fabric.Object#initialize} for constructor definition * * @fires added * @fires removed @@ -12970,14 +12806,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type String * @default */ - originX: 'center', + originX: 'left', /** * Vertical origin of transformation of an object (one of "top", "bottom", "center") * @type String * @default */ - originY: 'center', + originY: 'top', /** * Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom} @@ -13109,7 +12945,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type Boolean * @default */ - centeredRotation: false, + centeredRotation: true, /** * Color of object's fill @@ -13462,95 +13298,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this.toObject(propertiesToInclude); }, - /* _TO_SVG_START_ */ - /** - * Returns styles-string for svg-export - * @return {String} - */ - getSvgStyles: function() { - - var fill = this.fill - ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) - : 'none'; - - var stroke = this.stroke - ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) - : 'none'; - - var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; - var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; - var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; - var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; - var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; - var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; - - var visibility = this.visible ? '' : " visibility: hidden;"; - var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; - - return [ - "stroke: ", stroke, "; ", - "stroke-width: ", strokeWidth, "; ", - "stroke-dasharray: ", strokeDashArray, "; ", - "stroke-linecap: ", strokeLineCap, "; ", - "stroke-linejoin: ", strokeLineJoin, "; ", - "stroke-miterlimit: ", strokeMiterLimit, "; ", - "fill: ", fill, "; ", - "opacity: ", opacity, ";", - filter, - visibility - ].join(''); - }, - - /** - * Returns transform-string for svg-export - * @return {String} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - var center = this.getCenterPoint(); - - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var translatePart = "translate(" + - toFixed(center.x, NUM_FRACTION_DIGITS) + - " " + - toFixed(center.y, NUM_FRACTION_DIGITS) + - ")"; - - var anglePart = angle !== 0 - ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") - : ''; - - var scalePart = (this.scaleX === 1 && this.scaleY === 1) - ? '' : - (" scale(" + - toFixed(this.scaleX, NUM_FRACTION_DIGITS) + - " " + - toFixed(this.scaleY, NUM_FRACTION_DIGITS) + - ")"); - - var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; - var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; - - return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); - }, - - _createBaseSVGMarkup: function() { - var markup = [ ]; - - if (this.fill && this.fill.toLive) { - markup.push(this.fill.toSVG(this, false)); - } - if (this.stroke && this.stroke.toLive) { - markup.push(this.stroke.toSVG(this, false)); - } - if (this.shadow) { - markup.push(this.shadow.toSVG(this)); - } - return markup; - }, - /* _TO_SVG_END_ */ - /** * @private * @param {Object} object @@ -13677,6 +13424,30 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ctx.save(); + this._transform(ctx, noTransform); + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + + var m = this.transformMatrix; + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + this._render(ctx, noTransform); + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + + _transform: function(ctx, noTransform) { var m = this.transformMatrix; var v; if (this.canvas) { @@ -13691,11 +13462,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (m && !this.group) { ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); } - if (!noTransform) { this.transform(ctx); } + }, + _setStrokeStyles: function(ctx) { if (this.stroke) { ctx.lineWidth = this.strokeWidth; ctx.lineCap = this.strokeLineCap; @@ -13705,12 +13477,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ? this.stroke.toLive(ctx) : this.stroke; } + }, + _setFillStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive ? this.fill.toLive(ctx) : this.fill; } +<<<<<<< HEAD if (m && this.group) { ctx.translate(-this.group.width/2, -this.group.height/2); @@ -13743,6 +13518,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.drawControls(ctx); } ctx.restore(); +======= +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f }, /** @@ -13889,10 +13666,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.set('active', false); this.setPositionByOrigin(new fabric.Point(el.width / 2, el.height / 2), 'center', 'center'); + var originalCanvas = this.canvas; canvas.add(this); var data = canvas.toDataURL(options); this.set(origParams).setCoords(); + this.canvas = originalCanvas; canvas.dispose(); canvas = null; @@ -14089,7 +13868,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @chainable */ center: function () { - return this.centerH().centerV(); + this.canvas.centerObject(this); + return this; }, /** @@ -14102,81 +13882,18 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Moves an object to the bottom of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) */ - sendToBack: function() { - if (this.group) { - fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); - } - else { - this.canvas.sendToBack(this); - } - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - if (this.group) { - fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); - } - else { - this.canvas.bringToFront(this); - } - return this; - }, - - /** - * Moves an object down in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function(intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); - } - else { - this.canvas.sendBackwards(this, intersecting); - } - return this; - }, - - /** - * Moves an object up in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function(intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); - } - else { - this.canvas.bringForward(this, intersecting); - } - return this; - }, - - /** - * Moves an object to specified level in stack of drawn objects - * @param {Number} index New position of object - * @return {fabric.Object} thisArg - * @chainable - */ - moveTo: function(index) { - if (this.group) { - fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); - } - else { - this.canvas.moveTo(this, index); - } - return this; + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + return { + x: pointer.x - objectLeftTop.x, + y: pointer.y - objectLeftTop.y + }; } }); @@ -14226,20 +13943,22 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { - var cx = point.x, cy = point.y; + var cx = point.x, + cy = point.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; - if ( originX === "left" ) { - cx = point.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) )/ 2; + if (originX === "left") { + cx = point.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - cx = point.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + else if (originX === "right") { + cx = point.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - if ( originY === "top" ) { - cy = point.y +( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2; + if (originY === "top") { + cy = point.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - cy = point.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2; + else if (originY === "bottom") { + cy = point.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the reverse rotation to the point (it's already scaled properly) @@ -14254,20 +13973,22 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { - var x = center.x, y = center.y; + var x = center.x, + y = center.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; // Get the point coordinates - if ( originX === "left" ) { - x = center.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - x = center.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } - if ( originY === "top" ) { - y = center.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2; + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - y = center.y + ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2; + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the rotation to the point (it's already scaled properly) @@ -14305,29 +14026,32 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Returns the point in local coordinates - * @param {fabric.Point} The point relative to the global coordinate system + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ toLocalPoint: function(point, originX, originY) { - var center = this.getCenterPoint(); + var center = this.getCenterPoint(), + strokeWidth = this.stroke ? this.strokeWidth : 0, + x, y; - var x, y; - if (originX !== undefined && originY !== undefined) { - if ( originX === "left" ) { - x = center.x - (this.getWidth() + this.strokeWidth*this.scaleX) / 2; + if (originX && originY) { + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - x = center.x + (this.getWidth() + this.strokeWidth*this.scaleX)/ 2; + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } else { x = center.x; } - if ( originY === "top" ) { - y = center.y - (this.getHeight() + this.strokeWidth*this.scaleY) / 2; + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - y = center.y + (this.getHeight() + this.strokeWidth*this.scaleY)/ 2; + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } else { y = center.y; @@ -14367,7 +14091,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * @param {String} to One of left, center, right + * @param {String} to One of 'left', 'center', 'right' */ adjustPosition: function(to) { var angle = degreesToRadians(this.angle); @@ -14508,7 +14232,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Checks if point is inside the object - * @param {Object} point + * @param {fabric.Point} point Point to check against * @return {Boolean} true if point is inside the object */ containsPoint: function(point) { @@ -14549,8 +14273,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Helper method to determine how many cross points are between the 4 object edges * and the horizontal line determined by a point on canvas * @private - * @param {Object} point - * @param {Object} oCoords Coordinates of the image being evaluated + * @param {fabric.Point} point Point to check + * @param {Object} oCoords Coordinates of the object being evaluated */ _findCrossPoints: function(point, oCoords) { var b1, b2, a1, a2, xi, yi, @@ -14817,6 +14541,185 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati })(); +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group) { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else { + this.canvas.moveTo(this, index); + } + return this; + } +}); + + +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns styles-string for svg-export + * @return {String} + */ + getSvgStyles: function() { + + var fill = this.fill + ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) + : 'none'; + + var stroke = this.stroke + ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) + : 'none'; + + var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; + var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; + var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; + var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; + var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; + var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; + + var visibility = this.visible ? '' : " visibility: hidden;"; + var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + + return [ + "stroke: ", stroke, "; ", + "stroke-width: ", strokeWidth, "; ", + "stroke-dasharray: ", strokeDashArray, "; ", + "stroke-linecap: ", strokeLineCap, "; ", + "stroke-linejoin: ", strokeLineJoin, "; ", + "stroke-miterlimit: ", strokeMiterLimit, "; ", + "fill: ", fill, "; ", + "opacity: ", opacity, ";", + filter, + visibility + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @return {String} + */ + getSvgTransform: function() { + var toFixed = fabric.util.toFixed; + var angle = this.getAngle(); + var center = this.getCenterPoint(); + + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var translatePart = "translate(" + + toFixed(center.x, NUM_FRACTION_DIGITS) + + " " + + toFixed(center.y, NUM_FRACTION_DIGITS) + + ")"; + + var anglePart = angle !== 0 + ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") + : ''; + + var scalePart = (this.scaleX === 1 && this.scaleY === 1) + ? '' : + (" scale(" + + toFixed(this.scaleX, NUM_FRACTION_DIGITS) + + " " + + toFixed(this.scaleY, NUM_FRACTION_DIGITS) + + ")"); + + var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; + var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; + + return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function() { + var markup = [ ]; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + if (this.shadow) { + markup.push(this.shadow.toSVG(this)); + } + return markup; + } +}); +/* _TO_SVG_END_ */ + + /* Depends on `stateProperties` */ @@ -14867,12 +14770,19 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot (function(){ var getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians; + degreesToRadians = fabric.util.degreesToRadians, + isVML = typeof G_vmlCanvasManager !== 'undefined'; fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** - * Determines which one of the four corners has been clicked + * The object interactivity controls. + * @private + */ + _controlsVisibility: null, + + /** + * Determines which corner has been clicked * @private * @param {Event} e Event object * @param {Object} offset Canvas offset @@ -14889,6 +14799,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot for (var i in this.oCoords) { + if (!this.isControlVisible(i)) { + continue; + } + if (i === 'mtr' && !this.hasRotatingPoint) { continue; } @@ -15152,7 +15066,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ~~(h + padding2 + strokeWidth * sy) + 1 ); - if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) { + if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY @@ -15185,6 +15099,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var size = this.cornerSize, size2 = size / 2, strokeWidth2 = ~~(this.strokeWidth / 2), // half strokeWidth rounded down +<<<<<<< HEAD wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), this.canvas.viewportTransform, true), width = wh.x, height = wh.y, @@ -15198,6 +15113,19 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot methodName = this.transparentCorners ? 'strokeRect' : 'fillRect', transparent = this.transparentCorners, isVML = typeof G_vmlCanvasManager !== 'undefined'; +======= + left = -(this.width / 2), + top = -(this.height / 2), + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f ctx.save(); @@ -15207,6 +15135,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left +<<<<<<< HEAD _left = left - scaleOffset - strokeWidth2 - padding; _top = top - scaleOffset - strokeWidth2 - padding; @@ -15262,10 +15191,53 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot isVML || transparent || ctx.clearRect(_left, _top, size, size); ctx[methodName](_left, _top, size, size); +======= + this._drawControl('tl', ctx, methodName, + left - scaleOffsetX - strokeWidth2 - paddingX, + top - scaleOffsetY - strokeWidth2 - paddingY); + + // top-right + this._drawControl('tr', ctx, methodName, + left + width - scaleOffsetX + strokeWidth2 + paddingX, + top - scaleOffsetY - strokeWidth2 - paddingY); + + // bottom-left + this._drawControl('tr', ctx, methodName, + left - scaleOffsetX - strokeWidth2 - paddingX, + top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + + // bottom-right + this._drawControl('br', ctx, methodName, + left + width + scaleOffsetSizeX + strokeWidth2 + paddingX, + top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + + if (!this.get('lockUniScaling')) { + + // middle-top + this._drawControl('mt', ctx, methodName, + left + width/2 - scaleOffsetX, + top - scaleOffsetY - strokeWidth2 - paddingY); + + // middle-bottom + this._drawControl('mb', ctx, methodName, + left + width/2 - scaleOffsetX, + top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + + // middle-right + this._drawControl('mb', ctx, methodName, + left + width + scaleOffsetSizeX + strokeWidth2 + paddingX, + top + height/2 - scaleOffsetY); + + // middle-left + this._drawControl('ml', ctx, methodName, + left - scaleOffsetX - strokeWidth2 - paddingX, + top + height/2 - scaleOffsetY); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } // middle-top-rotate if (this.hasRotatingPoint) { +<<<<<<< HEAD _left = left + width/2 - scaleOffset; _top = this.flipY ? @@ -15274,11 +15246,98 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot isVML || transparent || ctx.clearRect(_left, _top, size, size); ctx[methodName](_left, _top, size, size); +======= + this._drawControl('mtr', ctx, methodName, + left + width/2 - scaleOffsetX, + this.flipY + ? (top + height + (this.rotatingPointOffset / this.scaleY) - this.cornerSize/this.scaleX/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - this.cornerSize/this.scaleY/2 - strokeWidth2 - paddingY)); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } ctx.restore(); return this; + }, + + /** + * @private + */ + _drawControl: function(control, ctx, methodName, left, top) { + var sizeX = this.cornerSize / this.scaleX, + sizeY = this.cornerSize / this.scaleY; + + if (this.isControlVisible(control)) { + isVML || this.transparentCorners || ctx.clearRect(left, top, sizeX, sizeY); + ctx[methodName](left, top, sizeX, sizeY); + } + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlName) { + return this._getControlsVisibility()[controlName]; + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlName, visible) { + this._getControlsVisibility()[controlName] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + /** + * Returns the instance of the control visibility set for this object. + * @private + * @returns {Object} + */ + _getControlsVisibility: function() { + if (!this._controlsVisibility) { + this._controlsVisibility = { + tl: true, + tr: true, + br: true, + bl: true, + ml: true, + mt: true, + mr: true, + mb: true, + mtr: true + }; + } + return this._controlsVisibility; } }); })(); @@ -15526,6 +15585,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Line class * @class fabric.Line * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition */ fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { @@ -15593,7 +15653,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _render: function(ctx) { ctx.beginPath(); - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); } @@ -15749,6 +15809,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Circle class * @class fabric.Circle * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition */ fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { @@ -15945,6 +16006,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Triangle * @extends fabric.Object * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition */ fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { @@ -16072,6 +16134,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Ellipse * @extends fabric.Object * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition */ fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { @@ -16260,6 +16323,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Rect * @extends fabric.Object * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition */ fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { @@ -16348,7 +16412,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot y = -this.height / 2, w = this.width, h = this.height, - isInPathGroup = this.group && this.group.type !== 'group'; + isInPathGroup = this.group && this.group.type === 'path-group'; ctx.beginPath(); ctx.globalAlpha = isInPathGroup ? (ctx.globalAlpha * this.opacity) : this.opacity; @@ -16525,8 +16589,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot "use strict"; var fabric = global.fabric || (global.fabric = { }), - toFixed = fabric.util.toFixed, - min = fabric.util.array.min; + toFixed = fabric.util.toFixed; if (fabric.Polyline) { fabric.warn('fabric.Polyline is already defined'); @@ -16537,6 +16600,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Polyline class * @class fabric.Polyline * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition */ fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { @@ -16549,10 +16613,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Constructor - * @param {Array} points Array of points + * @param {Array} points Array of points (where each point is an object with x and y) * @param {Object} [options] Options object * @param {Boolean} [skipOffset] Whether points offsetting should be skipped * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); */ initialize: function(points, options, skipOffset) { options = options || { }; @@ -16669,18 +16746,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES), - minX = min(points, 'x'), - minY = min(points, 'y'); + parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES); - minX = minX < 0 ? minX : 0; - minY = minX < 0 ? minY : 0; - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2 + minX) || 0; - points[i].y -= (options.height / 2 + minY) || 0; - } + fabric.util.normalizePoints(points, options); return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options), true); }; @@ -16720,6 +16788,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Polygon class * @class fabric.Polygon * @extends fabric.Object + * @see {@link fabric.Polygon#initialize} for constructor definition */ fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ { @@ -16879,18 +16948,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES), - minX = min(points, 'x'), - minY = min(points, 'y'); + parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); - minX = minX < 0 ? minX : 0; - minY = minX < 0 ? minY : 0; - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2 + minX) || 0; - points[i].y -= (options.height / 2 + minY) || 0; - } + fabric.util.normalizePoints(points, options); return new fabric.Polygon(points, extend(parsedAttributes, options), true); }; @@ -16963,6 +17023,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Path * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition */ fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { @@ -17066,7 +17127,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot tempControlX, tempControlY, l = -((this.width / 2) + this.pathOffset.x), - t = -((this.height / 2) + this.pathOffset.y); + t = -((this.height / 2) + this.pathOffset.y), + methodName; for (var i = 0, len = this.path.length; i < len; ++i) { @@ -17110,14 +17172,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot x += current[1]; y += current[2]; // draw a line if previous command was moveTo as well (otherwise, it will have no effect) - ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t); + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; // draw a line if previous command was moveTo as well (otherwise, it will have no effect) - ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t); + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); break; case 'c': // bezierCurveTo, relative @@ -17171,7 +17239,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot tempY + t ); // set control point to 2nd one of this command - // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." controlX = x + current[1]; controlY = y + current[2]; @@ -17197,7 +17267,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot y = tempY; // set control point to 2nd one of this command - // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." controlX = current[1]; controlY = current[2]; @@ -17343,40 +17415,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; - - var v; - if (this.canvas) { - v = this.canvas.viewportTransform; - } - else { - v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution - } - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } if (!noTransform) { this.transform(ctx); } - // ctx.globalCompositeOperation = this.fillRule; - - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx) - : this.fill; - } - - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - } - + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); ctx.beginPath(); @@ -17386,14 +17432,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._renderStroke(ctx); this.clipTo && ctx.restore(); this._removeShadow(ctx); - ctx.restore(); - ctx.save(); if (!noTransform && this.active) { - var center; - center = fabric.util.transformPoint(this.getCenterPoint(), v); - ctx.translate(center.x, center.y); - ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -17533,49 +17573,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _parseDimensions: function() { var aX = [], aY = [], - previousX, - previousY, - isLowerCase = false, - x, - y; + previous = { }; this.path.forEach(function(item, i) { - if (item[0] !== 'H') { - previousX = (i === 0) ? getX(item) : getX(this.path[i-1]); - } - if (item[0] !== 'V') { - previousY = (i === 0) ? getY(item) : getY(this.path[i-1]); - } - - // lowercased letter denotes relative position; - // transform to absolute - if (item[0] === item[0].toLowerCase()) { - isLowerCase = true; - } - - // last 2 items in an array of coordinates are the actualy x/y (except H/V); - // collect them - - // TODO (kangax): support relative h/v commands - - x = isLowerCase - ? previousX + getX(item) - : item[0] === 'V' - ? previousX - : getX(item); - - y = isLowerCase - ? previousY + getY(item) - : item[0] === 'H' - ? previousY - : getY(item); - - var val = parseInt(x, 10); - if (!isNaN(val)) aX.push(val); - - val = parseInt(y, 10); - if (!isNaN(val)) aY.push(val); - + this._getCoordsFromCommand(item, i, aX, aY, previous); }, this); var minX = min(aX), @@ -17593,6 +17594,51 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }; return o; + }, + + _getCoordsFromCommand: function(item, i, aX, aY, previous) { + var isLowerCase = false; + + if (item[0] !== 'H') { + previous.x = (i === 0) ? getX(item) : getX(this.path[i - 1]); + } + if (item[0] !== 'V') { + previous.y = (i === 0) ? getY(item) : getY(this.path[i - 1]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + var xy = this._getXY(item, isLowerCase, previous); + + var val = parseInt(xy.x, 10); + if (!isNaN(val)) aX.push(val); + + val = parseInt(xy.y, 10); + if (!isNaN(val)) aY.push(val); + }, + + _getXY: function(item, isLowerCase, previous) { + + // last 2 items in an array of coordinates are the actualy x/y (except H/V), collect them + // TODO (kangax): support relative h/v commands + + var x = isLowerCase + ? previous.x + getX(item) + : item[0] === 'V' + ? previous.x + : getX(item); + + var y = isLowerCase + ? previous.y + getY(item) + : item[0] === 'H' + ? previous.y + : getY(item); + + return { x: x, y: y }; } }); @@ -17676,6 +17722,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.PathGroup * @extends fabric.Path * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} + * @see {@link fabric.PathGroup#initialize} for constructor definition */ fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @lends fabric.PathGroup.prototype */ { @@ -17727,16 +17774,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; - - var v; - if (this.canvas) { - v = this.canvas.viewportTransform; - } - else { - v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution - } - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -17750,14 +17787,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this.clipTo && ctx.restore(); this._removeShadow(ctx); - ctx.restore(); - ctx.save(); if (this.active) { - var center; - center = fabric.util.transformPoint(this.getCenterPoint(), v); - ctx.translate(center.x, center.y); - ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -17920,8 +17951,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot extend = fabric.util.object.extend, min = fabric.util.array.min, max = fabric.util.array.max, - invoke = fabric.util.array.invoke, - degreesToRadians = fabric.util.degreesToRadians; + invoke = fabric.util.array.invoke; if (fabric.Group) { return; @@ -17945,6 +17975,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @extends fabric.Object * @mixes fabric.Collection * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#groups} + * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { @@ -17967,7 +17998,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._objects = objects || []; for (var i = this._objects.length; i--; ) { this._objects[i].group = this; - this._objects[i].setCoords(); } this.originalState = { }; @@ -17989,26 +18019,28 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @private */ _updateObjectsCoords: function() { - var groupDeltaX = this.left, - groupDeltaY = this.top; + this.forEachObject(this._updateObjectCoords, this); + }, - this.forEachObject(function(object) { + /** + * @private + */ + _updateObjectCoords: function(object) { + var objectLeft = object.getLeft(), + objectTop = object.getTop(); - var objectLeft = object.get('left'), - objectTop = object.get('top'); + object.set({ + originalLeft: objectLeft, + originalTop: objectTop, + left: objectLeft - this.left, + top: objectTop - this.top + }); - object.set('originalLeft', objectLeft); - object.set('originalTop', objectTop); + object.setCoords(); - object.set('left', objectLeft - groupDeltaX); - object.set('top', objectTop - groupDeltaY); - - object.setCoords(); - - // do not display corners of objects enclosed in a group - object.__origHasControls = object.hasControls; - object.hasControls = false; - }, this); + // do not display corners of objects enclosed in a group + object.__origHasControls = object.hasControls; + object.hasControls = false; }, /** @@ -18019,14 +18051,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return '#'; }, - /** - * Returns an array of all objects in this group - * @return {Array} group objects - */ - getObjects: function() { - return this._objects; - }, - /** * Adds an object to a group; Then recalculates group's dimension, position. * @param {Object} object @@ -18038,12 +18062,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._objects.push(object); object.group = this; // since _restoreObjectsState set objects inactive - this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this); + this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); return this; }, + /** + * @private + */ + _setObjectActive: function(object) { + object.set('active', true); + object.group = this; + }, + /** * Removes an object from a group; Then recalculates group's dimension, position. * @param {Object} object @@ -18053,12 +18085,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot removeWithUpdate: function(object) { this._moveFlippedObject(object); this._restoreObjectsState(); + // since _restoreObjectsState set objects inactive - this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this); + this.forEachObject(this._setObjectActive, this); this.remove(object); this._calcBounds(); this._updateObjectsCoords(); + return this; }, @@ -18131,42 +18165,45 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (!this.visible) return; ctx.save(); - var v = this.canvas.viewportTransform; - - var sxy = fabric.util.transformPoint( - new fabric.Point(this.scaleX, this.scaleY), - v, true), - groupScaleFactor = Math.max(sxy.x, sxy.y); + this.transform(ctx); this.clipTo && fabric.util.clipContext(this, ctx); - //The array is now sorted in order of highest first, so start from end. + // the array is now sorted in order of highest first, so start from end for (var i = 0, len = this._objects.length; i < len; i++) { - - var object = this._objects[i], - originalScaleFactor = object.borderScaleFactor, - originalHasRotatingPoint = object.hasRotatingPoint; - - // do not render if object is not visible - if (!object.visible) continue; - - object.hasRotatingPoint = false; - object.render(ctx); - - object.hasRotatingPoint = originalHasRotatingPoint; + this._renderObject(this._objects[i], ctx); } + this.clipTo && ctx.restore(); - if (this.active && !noTransform) { - var center = fabric.util.transformPoint(this.getCenterPoint(), v); - ctx.translate(center.x, center.y); - ctx.rotate(degreesToRadians(this.angle)); + if (!noTransform && this.active) { this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); }, + /** + * @private + */ + _renderObject: function(object, ctx) { + + var originalScaleFactor = object.borderScaleFactor, + originalHasRotatingPoint = object.hasRotatingPoint, + groupScaleFactor = Math.max(this.scaleX, this.scaleY); + + // do not render if object is not visible + if (!object.visible) return; + + object.borderScaleFactor = groupScaleFactor; + object.hasRotatingPoint = false; + + object.render(ctx); + + object.borderScaleFactor = originalScaleFactor; + object.hasRotatingPoint = originalHasRotatingPoint; + }, + /** * Retores original state of each of group objects (original state is that which was before group was created). * @private @@ -18185,9 +18222,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {fabric.Group} thisArg */ _moveFlippedObject: function(object) { - var oldOriginX = object.get('originX'); - var oldOriginY = object.get('originY'); - var center = object.getCenterPoint(); + var oldOriginX = object.get('originX'), + oldOriginY = object.get('originY'), + center = object.getCenterPoint(); + object.set({ originX: 'center', originY: 'center', @@ -18195,6 +18233,24 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot top: center.y }); + this._toggleFlipping(object); + + var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); + + object.set({ + originX: oldOriginX, + originY: oldOriginY, + left: newOrigin.x, + top: newOrigin.y + }); + + return this; + }, + + /** + * @private + */ + _toggleFlipping: function(object) { if (this.flipX) { object.toggle('flipX'); object.set('left', -object.get('left')); @@ -18205,15 +18261,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot object.set('top', -object.get('top')); object.setAngle(-object.getAngle()); } - - var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); - object.set({ - originX: oldOriginX, - originY: oldOriginY, - left: newOrigin.x, - top: newOrigin.y - }); - return this; }, /** @@ -18223,19 +18270,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {fabric.Group} thisArg */ _restoreObjectState: function(object) { - var groupLeft = this.get('left'), - groupTop = this.get('top'), - groupAngle = this.getAngle() * (Math.PI / 180), - rotatedTop = Math.cos(groupAngle) * object.get('top') * this.get('scaleY') + Math.sin(groupAngle) * object.get('left') * this.get('scaleX'), - rotatedLeft = -Math.sin(groupAngle) * object.get('top') * this.get('scaleY') + Math.cos(groupAngle) * object.get('left') * this.get('scaleX'); - - object.setAngle(object.getAngle() + this.getAngle()); - - object.set('left', groupLeft + rotatedLeft); - object.set('top', groupTop + rotatedTop); - - object.set('scaleX', object.get('scaleX') * this.get('scaleX')); - object.set('scaleY', object.get('scaleY') * this.get('scaleY')); + this._setObjectPosition(object); object.setCoords(); object.hasControls = object.__origHasControls; @@ -18247,6 +18282,37 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; }, + /** + * @private + */ + _setObjectPosition: function(object) { + var groupLeft = this.getLeft(), + groupTop = this.getTop(), + rotated = this._getRotatedLeftTop(object); + + object.set({ + angle: object.getAngle() + this.getAngle(), + left: groupLeft + rotated.left, + top: groupTop + rotated.top, + scaleX: object.get('scaleX') * this.get('scaleX'), + scaleY: object.get('scaleY') * this.get('scaleY') + }); + }, + + /** + * @private + */ + _getRotatedLeftTop: function(object) { + var groupAngle = this.getAngle() * (Math.PI / 180); + return { + left: (-Math.sin(groupAngle) * object.getTop() * this.get('scaleY') + + Math.cos(groupAngle) * object.getLeft() * this.get('scaleX')), + + top: (Math.cos(groupAngle) * object.getTop() * this.get('scaleY') + + Math.sin(groupAngle) * object.getLeft() * this.get('scaleX')) + }; + }, + /** * Destroys a group (restoring state of its objects) * @return {fabric.Group} thisArg @@ -18312,11 +18378,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _calcBounds: function() { var aX = [], aY = [], - minX, minY, maxX, maxY, o, width, height, minXY, maxXY, - i = 0, - len = this._objects.length; + o; - for (; i < len; ++i) { + for (var i = 0, len = this._objects.length; i < len; ++i) { o = this._objects[i]; o.setCoords(); for (var prop in o.oCoords) { @@ -18324,20 +18388,27 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot aY.push(o.oCoords[prop].y); } } - - var ivt = fabric.util.invertTransform(canvas.viewportTransform) || [1, 0, 0, 1, 0, 0]; - minXY = new fabric.Point(min(aX), min(aY)); - maxXY = new fabric.Point(max(aX), max(aY)); + this.set(this._getBounds(aX, aY)); + }, - minXY = fabric.util.transformPoint(minXY, ivt); - maxXY = fabric.util.transformPoint(maxXY, ivt); + /** + * @private + */ + _getBounds: function(aX, aY) { + var minX = min(aX), + maxX = max(aX), + minY = min(aY), + maxY = max(aY), + width = (maxX - minX) || 0, + height = (maxY - minY) || 0; - this.width = (maxXY.x - minXY.x) || 0; - this.height = (maxXY.y - minXY.y) || 0; - - this.left = (minXY.x + maxXY.x) / 2 || 0; - this.top = (minXY.y + maxXY.y) / 2 || 0; + return { + width: width, + height: height, + left: (minX + width / 2) || 0, + top: (minY + height / 2) || 0 + }; }, /* _TO_SVG_START_ */ @@ -18438,6 +18509,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Image * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#images} + * @see {@link fabric.Image#initialize} for constructor definition */ fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { @@ -18448,6 +18520,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ type: 'image', + /** + * crossOrigin value (one of "", "anonymous", "allow-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + /** * Constructor * @param {HTMLImageElement | String} element Image element @@ -18460,7 +18540,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this.filters = [ ]; this.callSuper('initialize', options); - this._initElement(element); + + this._initElement(element, options); this._initConfig(options); if (options.filters) { @@ -18498,6 +18579,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; }, + /** + * Sets crossOrigin value (on an instance and corresponding image element) + * @return {fabric.Image} thisArg + * @chainable + */ + setCrossOrigin: function(value) { + this.crossOrigin = value; + this._element.crossOrigin = value; + + return this; + }, + /** * Returns original size of an image * @return {Object} Object with "width" and "height" properties @@ -18521,16 +18614,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.save(); var m = this.transformMatrix; - var v; - if (this.canvas) { - v = this.canvas.viewportTransform; - } - else { - v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution - } - var isInPathGroup = this.group && this.group.type !== 'group'; - - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + var isInPathGroup = this.group && this.group.type === 'path-group'; // this._resetWidthHeight(); if (isInPathGroup) { @@ -18553,23 +18637,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._renderStroke(ctx); this.clipTo && ctx.restore(); ctx.restore(); - ctx.restore(); - ctx.save(); if (this.active && !noTransform) { - var center; - if (this.group) { - center = fabric.util.transformPoint(this.group.getCenterPoint(), v); - ctx.translate(center.x, center.y); - ctx.rotate(degreesToRadians(this.group.angle)); - } - center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); - if (this.group) { - center.x *= this.group.scaleX; - center.y *= this.group.scaleY; - } - ctx.translate(center.x, center.y); - ctx.rotate(fabric.util.degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } @@ -18582,14 +18651,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _stroke: function(ctx) { ctx.save(); - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - + this._setStrokeStyles(ctx); ctx.beginPath(); ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); ctx.closePath(); @@ -18607,13 +18669,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot h = this.height; ctx.save(); - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; + this._setStrokeStyles(ctx); ctx.beginPath(); fabric.util.drawDashedLine(ctx, x, y, x+w, y, this.strokeDashArray); @@ -18634,7 +18690,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot src: this._originalElement.src || this._originalElement._src, filters: this.filters.map(function(filterObj) { return filterObj && filterObj.toObject(); - }) + }), + crossOrigin: this.crossOrigin }); }, @@ -18657,7 +18714,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot '" transform="translate(' + (-this.width/2) + ' ' + (-this.height/2) + ')', '" width="', this.width, '" height="', this.height, - '">' + '" preserveAspectRatio="none"', + '>' ); if (this.stroke || this.strokeDashArray) { @@ -18800,6 +18858,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); this.setOptions(options); this._setWidthHeight(options); + this._element.crossOrigin = this.crossOrigin; }, /** @@ -18862,28 +18921,13 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Function} [callback] Callback to invoke when an image instance is created */ fabric.Image.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.src; - - /** @ignore */ - img.onload = function() { + fabric.util.loadImage(object.src, function(img) { fabric.Image.prototype._initFilters.call(object, object, function(filters) { object.filters = filters || [ ]; - var instance = new fabric.Image(img, object); callback && callback(instance); - img = img.onload = img.onerror = null; }); - }; - - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback(null, true); - img = img.onload = img.onerror = null; - }; - - img.src = src; + }, null, object.crossOrigin); }; /** @@ -18896,7 +18940,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot fabric.Image.fromURL = function(url, callback, imgOptions) { fabric.util.loadImage(url, function(img) { callback(new fabric.Image(img, imgOptions)); - }); + }, null, imgOptions && imgOptions.crossOrigin); }; /* _FROM_SVG_START_ */ @@ -19086,6 +19130,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Brightness * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Brightness = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { @@ -19162,6 +19214,41 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Convolute * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Convolute = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { @@ -19203,15 +19290,16 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Object} canvasEl Canvas element to apply filter to */ applyTo: function(canvasEl) { - var weights = this.matrix; - var context = canvasEl.getContext('2d'); - var pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height); - var side = Math.round(Math.sqrt(weights.length)); - var halfSide = Math.floor(side/2); - var src = pixels.data; - var sw = pixels.width; - var sh = pixels.height; + var weights = this.matrix, + context = canvasEl.getContext('2d'), + pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side/2), + src = pixels.data, + sw = pixels.width, + sh = pixels.height; // pad output by the convolution matrix var w = sw; @@ -19222,6 +19310,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag // go through the destination image pixels var alphaFac = this.opaque ? 1 : 0; + for (var y=0; y= 0 && scy < sh && scx >= 0 && scx < sw) { - var srcOff = (scy*sw+scx)*4; - var wt = weights[cy*side+cx]; - r += src[srcOff] * wt; - g += src[srcOff+1] * wt; - b += src[srcOff+2] * wt; - a += src[srcOff+3] * wt; - } + + /* jshint maxdepth:5 */ + if (scy < 0 || scy > sh || scx < 0 || scx > sw) continue; + + var srcOff = (scy*sw+scx)*4; + var wt = weights[cy*side+cx]; + + r += src[srcOff] * wt; + g += src[srcOff+1] * wt; + b += src[srcOff+2] * wt; + a += src[srcOff+3] * wt; } } dst[dstOff] = r; @@ -19291,6 +19385,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.GradientTransparency * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.GradientTransparency#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.GradientTransparency({ + * threshold: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.GradientTransparency = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { @@ -19303,7 +19405,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag /** * Constructor - * @memberOf fabric.Image.filters.GradientTransparency + * @memberOf fabric.Image.filters.GradientTransparency.prototype * @param {Object} [options] Options object * @param {Number} [options.threshold=100] Threshold value */ @@ -19365,6 +19467,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Grayscale * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Grayscale = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { @@ -19423,6 +19530,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Invert * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Invert = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { @@ -19479,6 +19591,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Mask * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Mask#initialize} for constructor definition */ fabric.Image.filters.Mask = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { @@ -19553,25 +19666,10 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Function} [callback] Callback to invoke when a mask filter instance is created */ fabric.Image.filters.Mask.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.mask.src; - - /** @ignore */ - img.onload = function() { + fabric.util.loadImage(object.mask.src, function(img) { object.mask = new fabric.Image(img, object.mask); - callback && callback(new fabric.Image.filters.Mask(object)); - img = img.onload = img.onerror = null; - }; - - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback(null, true); - img = img.onload = img.onerror = null; - }; - - img.src = src; + }); }; /** @@ -19597,6 +19695,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Noise * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Noise = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { @@ -19676,6 +19782,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Pixelate * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Pixelate = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { @@ -19780,6 +19894,15 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.RemoveWhite * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveWhite#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveWhite({ + * threshold: 40, + * distance: 140 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.RemoveWhite = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ { @@ -19872,6 +19995,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Sepia * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { @@ -19927,6 +20055,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Sepia2 * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia2(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia2 = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { @@ -19948,10 +20081,32 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag data = imageData.data, iLen = data.length, i, r, g, b; +<<<<<<< HEAD + ctx.save(); + var m = this.transformMatrix; + + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + // ctx.globalCompositeOperation = this.fillRule; +======= for (i = 0; i < iLen; i+=4) { r = data[i]; g = data[i + 1]; b = data[i + 2]; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; @@ -19971,6 +20126,26 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag return new fabric.Image.filters.Sepia2(); }; +<<<<<<< HEAD + this._render(ctx); + this._renderFill(ctx); + this._renderStroke(ctx); + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + ctx.restore(); + + ctx.save(); + if (!noTransform && this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, +======= })(typeof exports !== 'undefined' ? exports : this); @@ -19987,8 +20162,24 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @class fabric.Image.filters.Tint * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Tint#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example Tint filter with hex color and opacity + * var filter = new fabric.Image.filters.Tint({ + * color: '#3513B0', + * opacity: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Tint filter with rgba color + * var filter = new fabric.Image.filters.Tint({ + * color: 'rgba(53, 21, 176, 0.5)' + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Tint = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f /** * Filter type @@ -20109,6 +20300,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @extends fabric.Object * @return {fabric.Text} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#text} + * @see {@link fabric.Text#initialize} for constructor definition */ fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { @@ -20129,6 +20321,11 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag text: true }, + /** + * @private + */ + _reNewline: /\r?\n/, + /** * Retrieves object's fontSize * @method getFontSize @@ -20351,7 +20548,8 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag useNative: true, /** - * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) * as well as for history (undo/redo) purposes * @type Array */ @@ -20415,19 +20613,50 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ _render: function(ctx) { - var isInPathGroup = this.group && this.group.type !== 'group'; +<<<<<<< HEAD + var m = this.transformMatrix; + + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); +======= + var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } else if (isInPathGroup && this.transformMatrix) { ctx.translate(-this.group.width/2, -this.group.height/2); } +<<<<<<< HEAD + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + ctx.restore(); + + ctx.save(); + if (this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); + this.drawBorders(ctx); + this.drawControls(ctx); +======= if (typeof Cufon === 'undefined' || this.useNative === true) { this._renderViaNative(ctx); } else { this._renderViaCufon(ctx); +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f } }, @@ -20436,31 +20665,20 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderViaNative: function(ctx) { + var textLines = this.text.split(this._reNewline); this.transform(ctx, fabric.isLikelyNode); this._setTextStyles(ctx); - var textLines = this.text.split(/\r?\n/); - this.width = this._getTextWidth(ctx, textLines); this.height = this._getTextHeight(ctx, textLines); this.clipTo && fabric.util.clipContext(this, ctx); this._renderTextBackground(ctx, textLines); - - if (this.textAlign !== 'left' && this.textAlign !== 'justify') { - ctx.save(); - ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); - } - - ctx.save(); - this._setShadow(ctx); - this._renderTextFill(ctx, textLines); - this._renderTextStroke(ctx, textLines); - this._removeShadow(ctx); - ctx.restore(); + this._translateForTextAlign(ctx); + this._renderText(ctx, textLines); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.restore(); @@ -20473,6 +20691,30 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag this._totalLineHeight = 0; }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx, textLines) { + ctx.save(); + this._setShadow(ctx); + this._renderTextFill(ctx, textLines); + this._renderTextStroke(ctx, textLines); + this._removeShadow(ctx); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _translateForTextAlign: function(ctx) { + if (this.textAlign !== 'left' && this.textAlign !== 'justify') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -20499,22 +20741,12 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {CanvasRenderingContext2D} ctx Context to render on */ _setTextStyles: function(ctx) { - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx) - : this.fill; - } - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - } + this._setFillStyles(ctx); + this._setStrokeStyles(ctx); ctx.textBaseline = 'alphabetic'; - ctx.textAlign = this.textAlign; + if (!this.skipTextAlign) { + ctx.textAlign = this.textAlign; + } ctx.font = this._getFontDeclaration(); }, @@ -20535,7 +20767,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @return {Number} Maximum width of fabric.Text object */ _getTextWidth: function(ctx, textLines) { - var maxWidth = ctx.measureText(textLines[0]).width; + var maxWidth = ctx.measureText(textLines[0] || '|').width; for (var i = 1, len = textLines.length; i < len; i++) { var currentLineWidth = ctx.measureText(textLines[i]).width; @@ -20554,7 +20786,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Number} left Left position of text * @param {Number} top Top position of text */ - _drawChars: function(method, ctx, chars, left, top) { + _renderChars: function(method, ctx, chars, left, top) { ctx[method](chars, left, top); }, @@ -20567,15 +20799,24 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {Number} top Top position of text * @param {Number} lineIndex Index of a line in a text */ - _drawTextLine: function(method, ctx, line, left, top, lineIndex) { + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // lift the line by quarter of fontSize top -= this.fontSize / 4; +<<<<<<< HEAD + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + invoke = fabric.util.array.invoke, + degreesToRadians = fabric.util.degreesToRadians; +======= // short-circuit if (this.textAlign !== 'justify') { - this._drawChars(method, ctx, line, left, top, lineIndex); + this._renderChars(method, ctx, line, left, top, lineIndex); return; } +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f var lineWidth = ctx.measureText(line).width; var totalWidth = this.width; @@ -20590,12 +20831,12 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag var leftOffset = 0; for (var i = 0, len = words.length; i < len; i++) { - this._drawChars(method, ctx, words[i], left + leftOffset, top, lineIndex); + this._renderChars(method, ctx, words[i], left + leftOffset, top, lineIndex); leftOffset += ctx.measureText(words[i]).width + spaceWidth; } } else { - this._drawChars(method, ctx, line, left, top, lineIndex); + this._renderChars(method, ctx, line, left, top, lineIndex); } }, @@ -20618,6 +20859,13 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag return -this.height / 2; }, +<<<<<<< HEAD + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + this._objects[i].setCoords(); + } +======= /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -20625,6 +20873,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ _renderTextFill: function(ctx, textLines) { if (!this.fill && !this.skipFillStrokeCheck) return; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f this._boundaries = [ ]; var lineHeights = 0; @@ -20633,7 +20882,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; - this._drawTextLine( + this._renderTextLine( 'fillText', ctx, textLines[i], @@ -20668,7 +20917,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; - this._drawTextLine( + this._renderTextLine( 'strokeText', ctx, textLines[i], @@ -20832,26 +21081,59 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag if (!this.visible) return; ctx.save(); - var v; - if (this.canvas) { - v = this.canvas.viewportTransform; +<<<<<<< HEAD + var v = this.canvas.viewportTransform; + + var sxy = fabric.util.transformPoint( + new fabric.Point(this.scaleX, this.scaleY), + v, true), + groupScaleFactor = Math.max(sxy.x, sxy.y); + + this.clipTo && fabric.util.clipContext(this, ctx); + + //The array is now sorted in order of highest first, so start from end. + for (var i = 0, len = this._objects.length; i < len; i++) { + + var object = this._objects[i], + originalScaleFactor = object.borderScaleFactor, + originalHasRotatingPoint = object.hasRotatingPoint; + + // do not render if object is not visible + if (!object.visible) continue; + + object.hasRotatingPoint = false; + object.render(ctx); + + object.hasRotatingPoint = originalHasRotatingPoint; } - else { - v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution - } - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - this._render(ctx); - ctx.restore(); - ctx.save(); - if (!noTransform && this.active) { - var center; - center = fabric.util.transformPoint(this.getCenterPoint(), v); + this.clipTo && ctx.restore(); + + if (this.active && !noTransform) { + var center = fabric.util.transformPoint(this.getCenterPoint(), v); ctx.translate(center.x, center.y); - ctx.rotate(fabric.util.degreesToRadians(this.angle)); + ctx.rotate(degreesToRadians(this.angle)); +======= + this._render(ctx); + if (!noTransform && this.active) { +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); +<<<<<<< HEAD + }, + + /** + * Retores original state of each of group objects (original state is that which was before group was created). + * @private + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + this._objects.forEach(this._restoreObjectState, this); + return this; +======= +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f }, /** @@ -20887,22 +21169,43 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ toSVG: function(reviver) { var markup = [ ], - textLines = this.text.split(/\r?\n/), - lineTopOffset = this.useNative + textLines = this.text.split(this._reNewline), + offsets = this._getSVGLeftTopOffsets(textLines), + textAndBg = this._getSVGTextAndBg(offsets.lineTop, offsets.textLeft, textLines), + shadowSpans = this._getSVGShadows(offsets.lineTop, textLines); + + // move top offset by an ascent + offsets.textTop += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + this._wrapSVGTextAndBg(markup, textAndBg, shadowSpans, offsets); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function(textLines) { + var lineTop = this.useNative ? this.fontSize * this.lineHeight : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), - textLeftOffset = -(this.width/2), - textTopOffset = this.useNative + textLeft = -(this.width/2), + textTop = this.useNative ? this.fontSize - 1 - : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight; - textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), - shadowSpans = this._getSVGShadows(lineTopOffset, textLines); - - // move top offset by an ascent - textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + return { + textLeft: textLeft, + textTop: textTop, + lineTop: lineTop + }; + }, + /** + * @private + */ + _wrapSVGTextAndBg: function(markup, textAndBg, shadowSpans, offsets) { markup.push( '', textAndBg.textBgRects.join(''), @@ -20914,23 +21217,21 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag (this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ': ''), 'style="', this.getSvgStyles(), '" ', /* svg starts from left/bottom corner so we normalize height */ - 'transform="translate(', toFixed(textLeftOffset, 2), ' ', toFixed(textTopOffset, 2), ')">', + 'transform="translate(', toFixed(offsets.textLeft, 2), ' ', toFixed(offsets.textTop, 2), ')">', shadowSpans.join(''), textAndBg.textSpans.join(''), '', '' ); - - return reviver ? reviver(markup.join('')) : markup.join(''); }, /** * @private - * @param {Number} lineTopOffset Line top offset + * @param {Number} lineHeight * @param {Array} textLines Array of all text lines * @return {Array} */ - _getSVGShadows: function(lineTopOffset, textLines) { + _getSVGShadows: function(lineHeight, textLines) { var shadowSpans = [], i, len, lineTopOffsetMultiplier = 1; @@ -20947,14 +21248,15 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag toFixed((lineLeftOffset + lineTopOffsetMultiplier) + this.shadow.offsetX, 2), ((i === 0 || this.useNative) ? '" y' : '" dy'), '="', toFixed(this.useNative - ? ((lineTopOffset * i) - this.height / 2 + this.shadow.offsetY) - : (lineTopOffset + (i === 0 ? this.shadow.offsetY : 0)), 2), + ? ((lineHeight * i) - this.height / 2 + this.shadow.offsetY) + : (lineHeight + (i === 0 ? this.shadow.offsetY : 0)), 2), '" ', this._getFillAttributes(this.shadow.color), '>', fabric.util.string.escapeXml(textLines[i]), ''); lineTopOffsetMultiplier = 1; - } else { + } + else { // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier // prevents empty tspans lineTopOffsetMultiplier++; @@ -20966,15 +21268,105 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag /** * @private - * @param {Number} lineTopOffset Line top offset + * @param {Number} lineHeight * @param {Number} textLeftOffset Text left offset * @param {Array} textLines Array of all text lines * @return {Object} */ - _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { - var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; +<<<<<<< HEAD + _calcBounds: function() { + var aX = [], + aY = [], + minX, minY, maxX, maxY, o, width, height, minXY, maxXY, + i = 0, + len = this._objects.length; +======= + _getSVGTextAndBg: function(lineHeight, textLeftOffset, textLines) { + var textSpans = [ ], + textBgRects = [ ], + lineTopOffsetMultiplier = 1; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + this._setSVGTextLineText(textLines[i], i, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + lineTopOffsetMultiplier = 1; + } + else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } +<<<<<<< HEAD + } + + var ivt = fabric.util.invertTransform(canvas.viewportTransform) || [1, 0, 0, 1, 0, 0]; + + minXY = new fabric.Point(min(aX), min(aY)); + maxXY = new fabric.Point(max(aX), max(aY)); + + minXY = fabric.util.transformPoint(minXY, ivt); + maxXY = fabric.util.transformPoint(maxXY, ivt); + + this.width = (maxXY.x - minXY.x) || 0; + this.height = (maxXY.y - minXY.y) || 0; + + this.left = (minXY.x + maxXY.x) / 2 || 0; + this.top = (minXY.y + maxXY.y) / 2 || 0; +======= + + if (!this.textBackgroundColor || !this._boundaries) continue; + + this._setSVGTextLineBg(textBgRects, i, textLeftOffset, lineHeight); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + _setSVGTextLineText: function(textLine, i, textSpans, lineHeight, lineTopOffsetMultiplier) { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) + ? toFixed(this._boundaries[i].left, 2) + : 0; + + textSpans.push( + ' elements since setting opacity + // on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLine), + '' + ); + }, + + _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) { + textBgRects.push( + ''); + }, + + _setSVGBg: function(textBgRects) { if (this.backgroundColor && this._boundaries) { textBgRects.push( ''); } - - // text and text-background - for (i = 0, len = textLines.length; i < len; i++) { - if (textLines[i] !== '') { - lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; - textSpans.push( - ' elements since setting opacity on containing one doesn't work in Illustrator - this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(textLines[i]), - '' - ); - lineTopOffsetMultiplier = 1; - } - else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - - if (!this.textBackgroundColor || !this._boundaries) continue; - - textBgRects.push( - ''); - } - return { - textSpans: textSpans, - textBgRects: textBgRects - }; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f }, /** @@ -21138,85 +21488,2713 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag })(typeof exports !== 'undefined' ? exports : this); -/** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ -fabric.util.object.extend(fabric.Text.prototype, { - _renderViaCufon: function(ctx) { +(function() { - var o = Cufon.textOptions || (Cufon.textOptions = { }); + var clone = fabric.util.object.clone; - // export options to be used by cufon.js - o.left = this.left; - o.top = this.top; - o.context = ctx; - o.color = this.fill; + /** + * IText class (introduced in v1.4) + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires text:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

Supported key combinations:

+ *
+    *   Move cursor:                    left, right, up, down
+    *   Select character:               shift + left, shift + right
+    *   Select text vertically:         shift + up, shift + down
+    *   Move cursor by word:            alt + left, alt + right
+    *   Select words:                   shift + alt + left, shift + alt + right
+    *   Move cursor to line start/end:  cmd + left, cmd + right
+    *   Select till start/end of line:  cmd + shift + left, cmd + shift + right
+    *   Jump to start/end of text:      cmd + up, cmd + down
+    *   Select till start/end of text:  cmd + shift + up, cmd + shift + down
+    *   Delete character:               backspace
+    *   Delete word:                    alt + backspace
+    *   Delete line:                    cmd + backspace
+    *   Forward delete:                 delete
+    *   Copy text:                      ctrl/cmd + c
+    *   Paste text:                     ctrl/cmd + v
+    *   Cut text:                       ctrl/cmd + x
+    * 
+ * + *

Supported mouse/touch combination

+ *
+    *   Position cursor:                click/touch
+    *   Create selection:               click/touch & drag
+    *   Create selection:               click & shift + click
+    *   Select word:                    double click
+    *   Select line:                    triple click
+    * 
+ */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { - var el = this._initDummyElementForCufon(); + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', - // set "cursor" to top/left corner - this.transform(ctx); + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Nubmer + * @default + */ + selectionStart: 0, - // draw text - Cufon.replaceElement(el, { - engine: 'canvas', - separate: 'none', - fontFamily: this.fontFamily, - fontWeight: this.fontWeight, - textDecoration: this.textDecoration, - textShadow: this.shadow && this.shadow.toString(), - textAlign: this.textAlign, - fontStyle: this.fontStyle, - lineHeight: this.lineHeight, - stroke: this.stroke, - strokeWidth: this.strokeWidth, - backgroundColor: this.backgroundColor, - textBackgroundColor: this.textBackgroundColor - }); + /** + * Index where text selection ends + * @type Nubmer + * @default + */ + selectionEnd: 0, - // update width, height - this.width = o.width; - this.height = o.height; + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', - this._totalLineHeight = o.totalLineHeight; - this._fontAscent = o.fontAscent; - this._boundaries = o.boundaries; + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, - el = null; + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, - // need to set coords _after_ the width/height was retreived from Cufon - this.setCoords(); + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of default cursor (when not overwritten by character style) + * @type String + * @default + */ + cursorColor: '#333', + +<<<<<<< HEAD + ctx.save(); + var m = this.transformMatrix; + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + var isInPathGroup = this.group && this.group.type !== 'group'; + + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + + // this._resetWidthHeight(); + if (isInPathGroup) { + ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); + } + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + + ctx.save(); + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + this._render(ctx); + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + this._renderStroke(ctx); + this.clipTo && ctx.restore(); + ctx.restore(); + ctx.restore(); + + ctx.save(); + if (this.active && !noTransform) { + var center; + if (this.group) { + center = fabric.util.transformPoint(this.group.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.group.angle)); + } + center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); + if (this.group) { + center.x *= this.group.scaleX; + center.y *= this.group.scaleY; + } + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, +======= + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Object containing character styles + * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) + * @type Object + * @default + */ + styles: null, + + skipFillStrokeCheck: true, +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _fontSizeFraction: 4, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + _charWidthsCache: { }, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.callSuper('initialize', text, options); + this.initBehavior(); + + fabric.IText.instances.push(this); + + // caching + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * Returns true if object has no styling + */ + isEmptyStyles: function() { + if (!this.styles) return true; + var obj = this.styles; + + for (var p1 in obj) { + for (var p2 in obj[p1]) { + /*jshint unused:false */ + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + this.selectionStart = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + this.selectionEnd = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index); + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * @return {Object} styles Style object at a cursor position + */ + getSelectionStyles: function() { + var loc = this.get2DCursorLocation(); + if (this.styles[loc.lineIndex]) { + return this.styles[loc.lineIndex][loc.charIndex] || { }; + } + return { }; + }, + + /** + * Sets style of a current selection + * @param {Object} [styles] Styles object + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles) { + if (this.selectionStart === this.selectionEnd) { + this._extendStyles(this.selectionStart, styles); + } + else { + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + this._extendStyles(i, styles); + } + } + return this; + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this.styles[loc.lineIndex]) { + this.styles[loc.lineIndex] = { }; + } + if (!this.styles[loc.lineIndex][loc.charIndex]) { + this.styles[loc.lineIndex][loc.charIndex] = { }; + } + + fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + this.ctx = ctx; + this.isEditing && this.renderCursorOrSelection(); + }, + + /** + * Renders cursor or selection (depending on what exists) + */ + renderCursorOrSelection: function() { + if (!this.active) return; + + var chars = this.text.split(''), + boundaries; + + if (this.selectionStart === this.selectionEnd) { + boundaries = this._getCursorBoundaries(chars, 'cursor'); + this.renderCursor(boundaries); + } + else { + boundaries = this._getCursorBoundaries(chars, 'selection'); + this.renderSelection(chars, boundaries); + } + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + */ + get2DCursorLocation: function(selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var textBeforeCursor = this.text.slice(0, selectionStart); + var linesBeforeCursor = textBeforeCursor.split(this._reNewline); + + return { + lineIndex: linesBeforeCursor.length - 1, + charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length + }; + }, + + /** + * Returns fontSize of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {Number} Character font size + */ + getCurrentCharFontSize: function(lineIndex, charIndex) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fontSize) || this.fontSize; + }, + + /** + * Returns color (fill) of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {String} Character color (fill) + */ + getCurrentCharColor: function(lineIndex, charIndex) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor; + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(chars, typeOfBoundaries) { + + var cursorLocation = this.get2DCursorLocation(), + + textLines = this.text.split(this._reNewline), + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + left = Math.round(this._getLeftOffset()), + top = -this.height / 2, + + offsets = this._getCursorBoundariesOffsets( + chars, typeOfBoundaries, cursorLocation, textLines); + + return { + left: left, + top: top, + leftOffset: offsets.left + offsets.lineLeft, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries, cursorLocation, textLines) { + + var lineLeftOffset = 0, + + lineIndex = 0, + charIndex = 0, + + leftOffset = 0, + topOffset = typeOfBoundaries === 'cursor' + // selection starts at the very top of the line, + // whereas cursor starts at the padding created by line height + ? (this._getHeightOfLine(this.ctx, 0) - + this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)) + : 0; + + for (var i = 0; i < this.selectionStart; i++) { + if (chars[i] === '\n') { + leftOffset = 0; + var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0); + topOffset += this._getCachedLineHeight(index); + + lineIndex++; + charIndex = 0; + } + else { + leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); + charIndex++; + } + + lineLeftOffset = this._getCachedLineOffset(lineIndex, textLines); + } + + this._clearCache(); + + return { + top: topOffset, + left: leftOffset, + lineLeft: lineLeftOffset + }; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * @private + */ + _getCachedLineHeight: function(index) { + return this.__lineHeights[index] || + (this.__lineHeights[index] = this._getHeightOfLine(this.ctx, index)); + }, + + /** + * @private + */ + _getCachedLineWidth: function(lineIndex, textLines) { + return this.__lineWidths[lineIndex] || + (this.__lineWidths[lineIndex] = this._getWidthOfLine(this.ctx, lineIndex, textLines)); + }, + + /** + * @private + */ + _getCachedLineOffset: function(lineIndex, textLines) { + var widthOfLine = this._getCachedLineWidth(lineIndex, textLines); + + return this.__lineOffsets[lineIndex] || + (this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine)); + }, + + /** + * Renders cursor + * @param {Object} boundaries + */ + renderCursor: function(boundaries) { + var ctx = this.ctx; + + ctx.save(); + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getCurrentCharFontSize(lineIndex, charIndex); + + ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); + ctx.globalAlpha = this._currentCursorOpacity; + + ctx.fillRect( + boundaries.left + boundaries.leftOffset, + boundaries.top + boundaries.topOffset, + this.cursorWidth / this.scaleX, + charHeight); + + ctx.restore(); + }, + + /** + * Renders text selection + * @param {Array} chars Array of characters + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + */ + renderSelection: function(chars, boundaries) { + var ctx = this.ctx; + + ctx.save(); + + ctx.fillStyle = this.selectionColor; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + textLines = this.text.split(this._reNewline), + origLineIndex = lineIndex; + + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + + if (chars[i] === '\n') { + boundaries.leftOffset = 0; + boundaries.topOffset += this._getHeightOfLine(ctx, lineIndex); + lineIndex++; + charIndex = 0; + } + else if (i !== this.text.length) { + + var charWidth = this._getWidthOfChar(ctx, chars[i], lineIndex, charIndex), + lineOffset = this._getLineLeftOffset(this._getWidthOfLine(ctx, lineIndex, textLines)) || 0; + + if (lineIndex === origLineIndex) { + // only offset the line if we're rendering selection of 2nd, 3rd, etc. line + lineOffset = 0; + } + + ctx.fillRect( + boundaries.left + boundaries.leftOffset + lineOffset, + boundaries.top + boundaries.topOffset, + charWidth, + this._getHeightOfLine(ctx, lineIndex)); + + boundaries.leftOffset += charWidth; + charIndex++; + } + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + + if (this.isEmptyStyles()) { + return this._renderCharsFast(method, ctx, line, left, top); + } + + this.skipTextAlign = true; + + // set proper box offset + left -= this.textAlign === 'center' + ? (this.width / 2) + : (this.textAlign === 'right') + ? this.width + : 0; + + // set proper line offset + var textLines = this.text.split(this._reNewline), + lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines), + lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth), + chars = line.split(''); + + left += lineLeftOffset || 0; + + ctx.save(); + for (var i = 0, len = chars.length; i < len; i++) { + this._renderChar(method, ctx, lineIndex, i, chars[i], left, top, lineHeight); + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderCharsFast: function(method, ctx, line, left, top) { + this.skipTextAlign = false; + + if (method === 'fillText' && this.fill) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + if (method === 'strokeText' && this.stroke) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { + var decl, charWidth; + + if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { + + var shouldStroke = decl.stroke || this.stroke, + shouldFill = decl.fill || this.fill; + + ctx.save(); + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl); + + if (shouldFill) { + ctx.fillText(_char, left, top); + } + if (shouldStroke) { + ctx.strokeText(_char, left, top); + } + + this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight); + ctx.restore(); + + ctx.translate(charWidth, 0); + } + else { + if (method === 'strokeText' && this.stroke) { + ctx[method](_char, left, top); + } + if (method === 'fillText' && this.fill) { + ctx[method](_char, left, top); + } + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i); + this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight); + + ctx.translate(ctx.measureText(_char).width, 0); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight) { + var textDecoration = styleDeclaration + ? (styleDeclaration.textDecoration || this.textDecoration) + : this.textDecoration; + + if (!textDecoration) return; + + if (textDecoration.indexOf('underline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + 0 + ); + } + if (textDecoration.indexOf('line-through') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + (lineHeight / this._fontSizeFraction) + ); + } + if (textDecoration.indexOf('overline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top, + charWidth, + lineHeight - (this.fontSize / this._fontSizeFraction) + ); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset) { + ctx.fillRect(left, top - offset, charWidth, 1); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine + top += this.fontSize / 4; + this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines + */ + _renderTextDecoration: function(ctx, textLines) { + if (this.isEmptyStyles()) { + return this.callSuper('_renderTextDecoration', ctx, textLines); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextLinesBackground: function(ctx, textLines) { + if (!this.textBackgroundColor && !this.styles) return; + + ctx.save(); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + } + + var lineHeights = 0, + fractionOfFontSize = this.fontSize / this._fontSizeFraction; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var heightOfLine = this._getHeightOfLine(ctx, i, textLines); + if (textLines[i] === '') { + lineHeights += heightOfLine; + continue; + } + + var lineWidth = this._getWidthOfLine(ctx, i, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset, + this._getTopOffset() + lineHeights + fractionOfFontSize, + lineWidth, + heightOfLine + ); + } + if (this.styles[i]) { + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { + + var _char = textLines[i][j]; + + ctx.fillStyle = this.styles[i][j].textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines), + this._getTopOffset() + lineHeights + fractionOfFontSize, + this._getWidthOfChar(ctx, _char, i, j, textLines) + 1, + heightOfLine + ); + } + } + } + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + */ + _getCacheProp: function(_char, styleDeclaration) { + return _char + + + styleDeclaration.fontFamily + + styleDeclaration.fontSize + + styleDeclaration.fontWeight + + styleDeclaration.fontStyle + + + styleDeclaration.shadow; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} _char + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} [decl] + */ + _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { + var styleDeclaration = decl || (this.styles[lineIndex] && this.styles[lineIndex][charIndex]); + + if (styleDeclaration) { + // cloning so that original style object is not polluted with following font declarations + styleDeclaration = clone(styleDeclaration); + } + else { + styleDeclaration = { }; + } + + this._applyFontStyles(styleDeclaration); + + var cacheProp = this._getCacheProp(_char, styleDeclaration); + + // short-circuit if no styles + if (this.isEmptyStyles() && this._charWidthsCache[cacheProp]) { + return this._charWidthsCache[cacheProp]; + } + + if (typeof styleDeclaration.shadow === 'string') { + styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); + } + + var fill = styleDeclaration.fill || this.fill; + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx) + : fill; + + if (styleDeclaration.stroke) { + ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) + ? styleDeclaration.stroke.toLive(ctx) + : styleDeclaration.stroke; + } + + ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; + ctx.font = this._getFontDeclaration.call(styleDeclaration); + this._setShadow.call(styleDeclaration, ctx); + + if (!this._charWidthsCache[cacheProp]) { + this._charWidthsCache[cacheProp] = ctx.measureText(_char).width; + } + return this._charWidthsCache[cacheProp]; + }, + + /** + * @private + * @param {Object} styleDeclaration + */ + _applyFontStyles: function(styleDeclaration) { + if (!styleDeclaration.fontFamily) { + styleDeclaration.fontFamily = this.fontFamily; + } + if (!styleDeclaration.fontSize) { + styleDeclaration.fontSize = this.fontSize; + } + if (!styleDeclaration.fontWeight) { + styleDeclaration.fontWeight = this.fontWeight; + } + if (!styleDeclaration.fontStyle) { + styleDeclaration.fontStyle = this.fontStyle; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { + ctx.save(); + var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); + ctx.restore(); + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfChar: function(ctx, _char, lineIndex, charIndex) { + if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) { + return this.styles[lineIndex][charIndex].fontSize || this.fontSize; + } + return this.fontSize; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getWidthOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getHeightOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) { + var width = 0; + for (var i = 0; i < charIndex; i++) { + width += this._getWidthOfCharAt(ctx, lineIndex, i, lines); + } + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfLine: function(ctx, lineIndex, textLines) { + // if (!this.styles[lineIndex]) { + // return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]); + // } + return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextWidth: function(ctx, textLines) { + + if (this.isEmptyStyles()) { + return this.callSuper('_getTextWidth', ctx, textLines); + } + + var maxWidth = this._getWidthOfLine(ctx, 0, textLines); + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = this._getWidthOfLine(ctx, i, textLines); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfLine: function(ctx, lineIndex, textLines) { + + textLines = textLines || this.text.split(this._reNewline); + + var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0); + + var line = textLines[lineIndex]; + var chars = line.split(''); + + for (var i = 1, len = chars.length; i < len; i++) { + var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i); + if (currentCharHeight > maxHeight) { + maxHeight = currentCharHeight; + } + } + + return maxHeight * this.lineHeight; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextHeight: function(ctx, textLines) { + var height = 0; + for (var i = 0, len = textLines.length; i < len; i++) { + height += this._getHeightOfLine(ctx, i, textLines); + } + return height; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTopOffset: function() { + var topOffset = fabric.Text.prototype._getTopOffset.call(this); + return topOffset - (this.fontSize / this._fontSizeFraction); + }, + + /** + * Returns object representation of an instance + * @methd toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { + styles: clone(this.styles) + }); + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @return {fabric.IText} instance of fabric.IText + */ + fabric.IText.fromObject = function(object) { + return new fabric.IText(object.text, clone(object)); + }; + + fabric.IText.instances = [ ]; + +})(); + + +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initKeyHandlers(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.initHiddenTextarea(); + }, + + /** + * Initializes "selected" event handler + */ + initSelectedHandler: function() { + this.on('selected', function() { + + var _this = this; + setTimeout(function() { + _this.selected = true; + }, 100); + + if (!this._hasCanvasHandlers) { + this._initCanvasHandlers(); + this._hasCanvasHandlers = true; + } + }); + }, + + /** + * @private + */ + _initCanvasHandlers: function() { + var _this = this; + + this.canvas.on('selection:cleared', function(options) { + + // do not exit editing if event fired + // when clicking on an object again (in editing mode) + if (options.e && _this.canvas.containsPoint(options.e, _this)) return; + + _this.exitEditing(); + }); + + this.canvas.on('mouse:up', function() { + this.getObjects('i-text').forEach(function(obj) { + obj.__isMousedown = false; + }); + }); + }, + + /** + * @private + */ + _tick: function() { + + var _this = this; + + if (this._abortCursorAnimation) return; + + this.animate('_currentCursorOpacity', 1, { + + duration: this.cursorDuration, + + onComplete: function() { + _this._onTickComplete(); + }, + + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, + + /** + * @private + */ + _onTickComplete: function() { + if (this._abortCursorAnimation) return; + + var _this = this; + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this.animate('_currentCursorOpacity', 0, { + duration: this.cursorDuration / 2, + onComplete: function() { + _this._tick(); + }, + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function() { + var _this = this; + if (this._cursorTimeout2) { + clearTimeout(this._cursorTimeout2); + } + this._cursorTimeout2 = setTimeout(function() { + _this._abortCursorAnimation = false; + _this._tick(); + }, this.cursorDelay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + this._abortCursorAnimation = true; + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + this.canvas && this.canvas.renderAll(); + + var _this = this; + setTimeout(function() { + _this._abortCursorAnimation = false; + }, 10); + }, + + /** + * Selects entire text + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this.text.length; + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this.text.slice(this.selectionStart, this.selectionEnd); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Surrent selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index--; + } + } + while (/\S/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index++; + } + } + while (/\S/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} current selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} current selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Returns number of newlines in selected text + * @return {Number} Number of newlines in selected text + */ + getNumNewLinesInSelectedText: function() { + var selectedText = this.getSelectedText(); + var numNewLines = 0; + for (var i = 0, chars = selectedText.split(''), len = chars.length; i < len; i++) { + if (chars[i] === '\n') { + numNewLines++; + } + } + return numNewLines; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction: 1 or -1 + */ + searchWordBoundary: function(selectionStart, direction) { + var index = selectionStart; + var _char = this.text.charAt(index); + var reNonWord = /[ \n\.,;!\?\-]/; + + while (!reNonWord.test(_char) && index > 0 && index < this.text.length) { + index += direction; + _char = this.text.charAt(index); + } + if (reNonWord.test(_char) && _char !== '\n') { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + var newSelectionStart = this.searchWordBoundary(selectionStart, -1); /* search backwards */ + var newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + */ + selectLine: function(selectionStart) { + var newSelectionStart = this.findLineBoundaryLeft(selectionStart); + var newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function() { + if (this.isEditing || !this.editable) return; + + this.exitEditingOnOthers(); + + this.isEditing = true; + + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + + this._tick(); + this.canvas && this.canvas.renderAll(); + + this.fire('editing:entered'); + + return this; + }, + + exitEditingOnOthers: function() { + fabric.IText.instances.forEach(function(obj) { + if (obj === this) return; + obj.exitEditing(); + }, this); + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * @private + */ + _updateTextarea: function() { + if (!this.hiddenTextarea) return; + + this.hiddenTextarea.value = this.text; + this.hiddenTextarea.selectionStart = this.selectionStart; + this.hiddenTextarea.focus(); + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) return; + + this.hoverCursor = this._savedProps.overCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + + this.selected = false; + this.isEditing = false; + this.selectable = true; + + this.selectionEnd = this.selectionStart; + this.hiddenTextarea && this.hiddenTextarea.blur(); + + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + + this.fire('editing:exited'); + + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + var textLines = this.text.split(this._reNewline); + for (var prop in this.styles) { + if (!textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * @private + */ + _removeCharsFromTo: function(start, end) { + + var i = end; + while (i !== start) { + + var prevIndex = this.get2DCursorLocation(i).charIndex; + i--; + var index = this.get2DCursorLocation(i).charIndex; + var isNewline = index > prevIndex; + + if (isNewline) { + this.removeStyleObject(isNewline, i + 1); + } + else { + this.removeStyleObject(this.get2DCursorLocation(i).charIndex === 0, i); + } + + } + + this.text = this.text.slice(0, start) + + this.text.slice(end); + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + * @param {String} _chars Characters to insert + */ + insertChars: function(_chars) { + var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n'; + + this.text = this.text.slice(0, this.selectionStart) + + _chars + + this.text.slice(this.selectionEnd); + + if (this.selectionStart === this.selectionEnd) { + this.insertStyleObject(_chars, isEndOfLine); + } + else if (this.selectionEnd - this.selectionStart > 1) { + // TODO: replace styles properly + // console.log('replacing MORE than 1 char'); + } + + this.selectionStart += _chars.length; + this.selectionEnd = this.selectionStart; + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('text:changed'); + }, + + /** + * Inserts new style object + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { + + this.shiftLineStyles(lineIndex, +1); + + if (!this.styles[lineIndex + 1]) { + this.styles[lineIndex + 1] = { }; + } + + var currentCharStyle = this.styles[lineIndex][charIndex - 1], + newLineStyles = { }; + + // if there's nothing after cursor, + // we clone current char style onto the next (otherwise empty) line + if (isEndOfLine) { + newLineStyles[0] = clone(currentCharStyle); + this.styles[lineIndex + 1] = newLineStyles; + } + // otherwise we clone styles of all chars + // after cursor onto the next line, from the beginning + else { + for (var index in this.styles[lineIndex]) { + if (parseInt(index, 10) >= charIndex) { + newLineStyles[parseInt(index, 10) - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + delete this.styles[lineIndex][index]; + } + } + this.styles[lineIndex + 1] = newLineStyles; + } + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + */ + insertCharStyleObject: function(lineIndex, charIndex) { + + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = clone(currentLineStyles); + + if (charIndex === 0) { + charIndex = 1; + } + + // shift all char styles by 1 forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex]; + //delete currentLineStyles[index]; + } + } + this.styles[lineIndex][charIndex] = clone(currentLineStyles[charIndex - 1]); + }, + + /** + * Inserts style object + * @param {String} _chars Characters at the location where style is inserted + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertStyleObject: function(_chars, isEndOfLine) { + + // short-circuit + if (this.isEmptyStyles()) return; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = { }; + } + + if (_chars === '\n') { + this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); + } + else { + // TODO: support multiple style insertion if _chars.length > 1 + this.insertCharStyleObject(lineIndex, charIndex); + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can be -1 or +1 + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by 1 upward + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + } + } + }, + + /** + * Removes style object + * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line + * @param {Number} [index] Optional index. When not given, current selectionStart is used. + */ + removeStyleObject: function(isBeginningOfLine, index) { + + var cursorLocation = this.get2DCursorLocation(index), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (isBeginningOfLine) { + + var textLines = this.text.split(this._reNewline), + textOnPreviousLine = textLines[lineIndex - 1], + newCharIndexOnPrevLine = textOnPreviousLine.length; + + if (!this.styles[lineIndex - 1]) { + this.styles[lineIndex - 1] = { }; + } + + for (charIndex in this.styles[lineIndex]) { + this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine] + = this.styles[lineIndex][charIndex]; + } + + this.shiftLineStyles(lineIndex, -1); + } + else { + var currentLineStyles = this.styles[lineIndex]; + + if (currentLineStyles) { + var offset = this.selectionStart === this.selectionEnd ? -1 : 0; + delete currentLineStyles[charIndex + offset]; + // console.log('deleting', lineIndex, charIndex + offset); + } + + var currentLineStylesCloned = clone(currentLineStyles); + + // shift all styles by 1 backwards + for (var i in currentLineStylesCloned) { + var numericIndex = parseInt(i, 10); + if (numericIndex >= charIndex && numericIndex !== 0) { + currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex]; + delete currentLineStyles[numericIndex]; + } + } + } + }, + + /** + * Inserts new line + */ + insertNewline: function() { + this.insertChars('\n'); + } + }); +})(); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.lastPointer = { }; + + this.on('mousedown', this.onMouseDown.bind(this)); + }, + + onMouseDown: function(options) { + + this.__newClickTime = +new Date(); + var newPointer = this.canvas.getPointer(options.e); + + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + else if (this.isDoubleClick(newPointer)) { + this.fire('dblclick', options); + this._stopEvent(options.e); + } + + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + }, + + isDoubleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; }, /** * @private */ - _initDummyElementForCufon: function() { - var el = fabric.document.createElement('pre'), - container = fabric.document.createElement('div'); + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, - // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent - container.appendChild(el); + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initSelectedHandler(); + this.initMousedownHandler(); + this.initMousemoveHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, - if (typeof G_vmlCanvasManager === 'undefined') { - el.innerHTML = this.text; + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('dblclick', function(options) { + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }); + this.on('tripleclick', function(options) { + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }); + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', function(options) { + + var pointer = this.canvas.getPointer(options.e); + + this.__mousedownX = pointer.x; + this.__mousedownY = pointer.y; + this.__isMousedown = true; + + if (this.hiddenTextarea && this.canvas) { + this.canvas.wrapperEl.appendChild(this.hiddenTextarea); + } + + if (this.isEditing) { + this.setCursorByClick(options.e); + this.__selectionStartOnMouseDown = this.selectionStart; + } + else { + this.exitEditingOnOthers(); + } + }); + }, + + /** + * Initializes "mousemove" event handler + */ + initMousemoveHandler: function() { + this.on('mousemove', function(options) { + if (!this.__isMousedown || !this.isEditing) return; + + var newSelectionStart = this.getSelectionStartFromPointer(options.e); + + if (newSelectionStart >= this.__selectionStartOnMouseDown) { + this.setSelectionStart(this.__selectionStartOnMouseDown); + this.setSelectionEnd(newSelectionStart); + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(this.__selectionStartOnMouseDown); + } + }); + }, + + /** + * @private + */ + _isObjectMoved: function(e) { + var pointer = this.canvas.getPointer(e); + + return this.__mousedownX !== pointer.x || + this.__mousedownY !== pointer.y; + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', function(options) { + this.__isMousedown = false; + + if (this._isObjectMoved(options.e)) return; + + if (this.selected) { + this.enterEditing(); + } + }); + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Object} pointer Pointer object with x and y numeric properties + */ + setCursorByClick: function(e) { + var newSelectionStart = this.getSelectionStartFromPointer(e); + + if (e.shiftKey) { + if (newSelectionStart < this.selectionStart) { + this.setSelectionEnd(this.selectionStart); + this.setSelectionStart(newSelectionStart); + } + else { + this.setSelectionEnd(newSelectionStart); + } } else { - // IE 7 & 8 drop newlines and white space on text nodes - // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html - // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp - el.innerText = this.text.replace(/\r?\n/gi, '\r'); + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionStart); + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {Object} Object with x/y corresponding to local offset (according to object rotation) + */ + _getLocalRotatedPointer: function(e) { + var pointer = this.canvas.getPointer(e), + + pClicked = new fabric.Point(pointer.x, pointer.y), + pLeftTop = new fabric.Point(this.left, this.top), + + rotated = fabric.util.rotatePoint( + pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle)); + + return this.getLocalPointer(e, rotated); + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + + var mouseOffset = this._getLocalRotatedPointer(e), + textLines = this.text.split(this._reNewline), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + newSelectionStart; + + for (var i = 0, len = textLines.length; i < len; i++) { + + height += this._getHeightOfLine(this.ctx, i) * this.scaleY; + + var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfLine); + + width = lineLeftOffset; + + if (this.flipX) { + // when oject is horizontally flipped we reverse chars + textLines[i] = textLines[i].split('').reverse().join(''); + } + + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + + var _char = textLines[i][j]; + prevWidth = width; + + width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) * + this.scaleX; + + if (height <= mouseOffset.y || width <= mouseOffset.x) { + charIndex++; + continue; + } + + return this._getNewSelectionStartFromOffset( + mouseOffset, prevWidth, width, charIndex + i, jlen); + } } - el.style.fontSize = this.fontSize + 'px'; - el.style.letterSpacing = 'normal'; + // clicked somewhere after all chars, so set at the end + if (typeof newSelectionStart === 'undefined') { + return this.text.length; + } + }, - return el; + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1, + newSelectionStart = index + offset; + + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this.text.length) { + newSelectionStart = this.text.length; + } + + return newSelectionStart; } }); +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes key handlers + */ + initKeyHandlers: function() { + fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(fabric.document, 'keypress', this.onKeyPress.bind(this)); + }, + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px'; + + fabric.document.body.appendChild(this.hiddenTextarea); + }, + + /** + * @private + */ + _keysMap: { + 8: 'removeChars', + 13: 'insertNewline', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + 46: 'forwardDelete' + }, + + /** + * @private + */ + _ctrlKeysMap: { + 65: 'selectAll', + 67: 'copy', + 86: 'paste', + 88: 'cut' + }, + + /** + * Handles keyup event + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) return; + + if (e.keyCode in this._keysMap) { + this[this._keysMap[e.keyCode]](e); + } + else if ((e.keyCode in this._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) { + this[this._ctrlKeysMap[e.keyCode]](e); + } + else { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.canvas && this.canvas.renderAll(); + }, + + /** + * Forward delete + */ + forwardDelete: function(e) { + if (this.selectionStart === this.selectionEnd) { + this.moveCursorRight(e); + } + this.removeChars(e); + }, + + /** + * Copies selected text + */ + copy: function() { + var selectedText = this.getSelectedText(); + this.copiedText = selectedText; + }, + + /** + * Pastes text + */ + paste: function() { + if (this.copiedText) { + this.insertChars(this.copiedText); + } + }, + + /** + * Cuts text + */ + cut: function(e) { + this.copy(); + this.removeChars(e); + }, + + /** + * Handles keypress event + * @param {Event} e Event object + */ + onKeyPress: function(e) { + if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) { + return; + } + + this.insertChars(String.fromCharCode(e.which)); + + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * Gets start offset of a selection + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset, + + textBeforeCursor = this.text.slice(0, selectionProp), + textAfterCursor = this.text.slice(selectionProp), + + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1], + textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '', + + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on last line, down cursor goes to end of line + if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey) { + + // move to the end of a text + return this.text.length - selectionProp; + } + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnNextLine = this._getIndexOnNextLine( + cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnSameLineAfterCursor.length + 1 + indexOnNextLine; + }, + + /** + * @private + */ + _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex + 1; + var widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfNextLine); + var widthOfCharsOnNextLine = lineLeftOffset; + var indexOnNextLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) { + + var _char = textOnNextLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnNextLine += widthOfChar; + + if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnNextLine - widthOfChar; + var rightEdge = widthOfCharsOnNextLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j; + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnNextLine = textOnNextLine.length; + } + + return indexOnNextLine; + }, + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorDownWithShift(offset); + } + else { + this.moveCursorDownWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor down without keeping selection + * @param {Number} offset + */ + moveCursorDownWithoutShift: function(offset) { + + this._selectionDirection = 'right'; + this.selectionStart += offset; + + if (this.selectionStart > this.text.length) { + this.selectionStart = this.text.length; + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor down while keeping selection + * @param {Number} offset + */ + moveCursorDownWithShift: function(offset) { + + if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) { + this.selectionStart += offset; + this._selectionDirection = 'left'; + return; + } + else { + this._selectionDirection = 'right'; + this.selectionEnd += offset; + + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + getUpCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on first line, up cursor goes to start of line + if (cursorLocation.lineIndex === 0 || e.metaKey) { + return selectionProp; + } + + var textBeforeCursor = this.text.slice(0, selectionProp), + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '', + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset; + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnPrevLine = this._getIndexOnPrevLine( + cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length; + }, + + /** + * @private + */ + _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex - 1; + var widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine); + var widthOfCharsOnPreviousLine = lineLeftOffset; + var indexOnPrevLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) { + + var _char = textOnPreviousLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnPreviousLine += widthOfChar; + + if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnPreviousLine - widthOfChar; + var rightEdge = widthOfCharsOnPreviousLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnPrevLine = textOnPreviousLine.length - 1; + } + + return indexOnPrevLine; + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorUpWithShift(offset); + } + else { + this.moveCursorUpWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor up with shift + * @param {Number} offset + */ + moveCursorUpWithShift: function(offset) { + + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + else { + if (this._selectionDirection === 'right') { + this.selectionEnd -= offset; + this._selectionDirection = 'right'; + return; + } + else { + this.selectionStart -= offset; + } + } + + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + + this._selectionDirection = 'left'; + }, + +<<<<<<< HEAD + ctx.save(); + var v; + if (this.canvas) { + v = this.canvas.viewportTransform; + } + else { + v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution + } + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._render(ctx); + ctx.restore(); + ctx.save(); + if (!noTransform && this.active) { + var center; + center = fabric.util.transformPoint(this.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(fabric.util.degreesToRadians(this.angle)); + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, +======= + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorUpWithoutShift: function(offset) { + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + this.selectionEnd = this.selectionStart; +>>>>>>> f84ac95f75347b051e52321b790b8aa936d7076f + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorLeftWithShift(e); + } + else { + this.moveCursorLeftWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * @private + */ + _move: function(e, prop, direction) { + if (e.altKey) { + this[prop] = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey) { + this[prop] = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += (direction === 'Left' ? -1 : 1); + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart) { + this._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + this._moveLeft(e, 'selectionEnd'); + } + else { + this._selectionDirection = 'left'; + this._moveLeft(e, 'selectionStart'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionStart) === '\n') { + this.selectionStart--; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorRightWithShift(e); + } + else { + this.moveCursorRightWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + } + else { + this._selectionDirection = 'right'; + this._moveRight(e, 'selectionEnd'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionEnd - 1) === '\n') { + this.selectionEnd++; + } + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e + */ + moveCursorRightWithoutShift: function(e) { + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += this.getNumNewLinesInSelectedText(); + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + this.selectionStart = this.selectionEnd; + } + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + */ + removeChars: function(e) { + if (this.selectionStart === this.selectionEnd) { + this._removeCharsNearCursor(e); + } + else { + this._removeCharsFromTo(this.selectionStart, this.selectionEnd); + } + + this.selectionEnd = this.selectionStart; + + this._removeExtraneousStyles(); + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('text:changed'); + }, + + /** + * @private + */ + _removeCharsNearCursor: function(e) { + if (this.selectionStart !== 0) { + + if (e.metaKey) { + // remove all till the start of current line + var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftLineBoundary, this.selectionStart); + this.selectionStart = leftLineBoundary; + } + else if (e.altKey) { + // remove all till the start of current word + var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftWordBoundary, this.selectionStart); + this.selectionStart = leftWordBoundary; + } + else { + var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n'; + this.removeStyleObject(isBeginningOfLine); + + this.selectionStart--; + this.text = this.text.slice(0, this.selectionStart) + + this.text.slice(this.selectionStart + 1); + } + } + } +}); + + +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * @private + */ + _setSVGTextLineText: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + if (!this.styles[lineIndex]) { + this.callSuper('_setSVGTextLineText', + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier); + } + else { + this._setSVGTextLineChars( + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + } + }, + + /** + * @private + */ + _setSVGTextLineChars: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + + var yProp = lineIndex === 0 || this.useNative ? 'y' : 'dy', + chars = textLine.split(''), + charOffset = 0, + lineLeftOffset = this._getSVGLineLeftOffset(lineIndex), + lineTopOffset = this._getSVGLineTopOffset(lineIndex), + heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); + + for (var i = 0, len = chars.length; i < len; i++) { + var styleDecl = this.styles[lineIndex][i] || { }; + + textSpans.push( + this._createTextCharSpan( + chars[i], styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset)); + + var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); + + if (styleDecl.textBackgroundColor) { + textBgRects.push( + this._createTextCharBg( + styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset)); + } + + charOffset += charWidth; + } + }, + + /** + * @private + */ + _getSVGLineLeftOffset: function(lineIndex) { + return (this._boundaries && this._boundaries[lineIndex]) + ? fabric.util.toFixed(this._boundaries[lineIndex].left, 2) + : 0; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0; + for (var j = 0; j <= lineIndex; j++) { + lineTopOffset += this._getHeightOfLine(this.ctx, j); + } + return lineTopOffset - this.height / 2; + }, + + /** + * @private + */ + _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { + return [ + '' + ].join(''); + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset) { + + var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ + visible: true, + fill: this.fill, + stroke: this.stroke, + type: 'text' + }, styleDecl)); + + return [ + '', + + fabric.util.string.escapeXml(_char), + '' + ].join(''); + } +}); +/* _TO_SVG_END_ */ + + (function() { if (typeof document !== 'undefined' && typeof window !== 'undefined') { @@ -21306,6 +24284,9 @@ fabric.util.object.extend(fabric.Text.prototype, { else if (url) { request(url, 'binary', createImageAndCallBack); } + else { + callback && callback.call(context, url); + } }; fabric.loadSVGFromURL = function(url, callback, reviver) { @@ -21415,6 +24396,6 @@ var exports = exports || {}; exports.fabric = fabric; if (typeof define === "function" && define.amd) { - define("fabric", [], function() { return fabric }); + define([], function() { return fabric }); } diff --git a/lib/event.js b/lib/event.js index 65120712..e7790348 100644 --- a/lib/event.js +++ b/lib/event.js @@ -1,1904 +1,1829 @@ /* - ---------------------------------------------------- - Event.js : 1.1.1 : 2012/11/19 : MIT License - ---------------------------------------------------- - https://github.com/mudcube/Event.js - ---------------------------------------------------- - 1 : click, dblclick, dbltap - 1+ : tap, longpress, drag, swipe - 2+ : pinch, rotate - : mousewheel, devicemotion, shake - ---------------------------------------------------- - TODO - ---------------------------------------------------- - * switch configuration to 4th argument on addEventListener - * bbox calculation for elements scaled with transform. - ---------------------------------------------------- - NOTES - ---------------------------------------------------- - * When using other libraries that may have built in "Event" namespace, - i.e. Typescript, you can use "eventjs" instead of "Event" for all example calls. - ---------------------------------------------------- - REQUIREMENTS: querySelector, querySelectorAll - ---------------------------------------------------- - * There are two ways to add/remove events with this library. - ---------------------------------------------------- - // Retains "this" attribute as target, and overrides native addEventListener. - target.addEventListener(type, listener, useCapture); - target.removeEventListener(type, listener, useCapture); + ---------------------------------------------------- + Event.js : 1.1.3 : 2013/07/17 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- + https://github.com/rykerwilliams/Event.js + ---------------------------------------------------- + 1 : click, dblclick, dbltap + 1+ : tap, longpress, drag, swipe + 2+ : pinch, rotate + : mousewheel, devicemotion, shake + ---------------------------------------------------- + Ideas for the future + ---------------------------------------------------- + * GamePad, and other input abstractions. + * Event batching - i.e. for every x fingers down a new gesture is created. + ---------------------------------------------------- + http://www.w3.org/TR/2011/WD-touch-events-20110505/ + ---------------------------------------------------- + +*/ - // Attempts to perform as fast as possible. - Event.add(type, listener, configure); - Event.remove(type, listener, configure); +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(eventjs) === "undefined") var eventjs = Event; - * You can turn prototyping on/off for individual features. - ---------------------------------------------------- - Event.modifyEventListener = true; // add custom *EventListener commands to HTMLElements. - Event.modifySelectors = true; // add bulk *EventListener commands on NodeLists from querySelectorAll and others. +(function(root) { "use strict"; - * Example of setting up a single listener with a custom configuration. - ---------------------------------------------------- - // optional configuration. - var configure = { - fingers: 2, // listen for specifically two fingers. - snap: 90 // snap to 90 degree intervals. - }; - // adding with addEventListener() - target.addEventListener("swipe", function(event) { - // additional variables can be found on the event object. - console.log(event.velocity, event.angle, event.fingers); - }, configure); +// Add custom *EventListener commands to HTMLElements (set false to prevent funkiness). +root.modifyEventListener = true; - // adding with Event.add() - Event.add("swipe", function(event, self) { - // additional variables can be found on the self object. - console.log(self.velocity, self.angle, self.fingers); - }, configure); - - * Multiple listeners glued together. - ---------------------------------------------------- - // adding with addEventListener() - target.addEventListener("click swipe", function(event) { }); - - // adding with Event.add() - Event.add(target, "click swipe", function(event, self) { }); - - * Use query selectors to create an event (querySelectorAll) - ---------------------------------------------------- - // adding events to NodeList from querySelectorAll() - document.querySelectorAll("#element a.link").addEventListener("click", callback); - - // adding with Event.add() - Event.add("#element a.link", "click", callback); - - * Listen for selector to become available (querySelector) - ---------------------------------------------------- - Event.add("body", "ready", callback); - // or... - Event.add({ - target: "body", - type: "ready", - timeout: 10000, // set a timeout to stop checking. - interval: 30, // set how often to check for element. - listener: callback - }); - - * Multiple listeners bound to one callback w/ single configuration. - ---------------------------------------------------- - var bindings = Event.add({ - target: target, - type: "click swipe", - snap: 90, // snap to 90 degree intervals. - minFingers: 2, // minimum required fingers to start event. - maxFingers: 4, // maximum fingers in one event. - listener: function(event, self) { - console.log(self.gesture); // will be click or swipe. - console.log(self.x); - console.log(self.y); - console.log(self.identifier); - console.log(self.start); - console.log(self.fingers); // somewhere between "2" and "4". - self.pause(); // disable event. - self.resume(); // enable event. - self.remove(); // remove event. - } - }); - - * Multiple listeners bound to multiple callbacks w/ single configuration. - ---------------------------------------------------- - var bindings = Event.add({ - target: target, - minFingers: 1, - maxFingers: 12, - listeners: { - click: function(event, self) { - self.remove(); // removes this click listener. - }, - swipe: function(event, self) { - binding.remove(); // removes both the click + swipe listeners. - } - } - }); - - * Multiple listeners bound to multiple callbacks w/ multiple configurations. - ---------------------------------------------------- - var binding = Event.add({ - target: target, - listeners: { - longpress: { - fingers: 1, - wait: 500, // milliseconds - listener: function(event, self) { - console.log(self.fingers); // "1" finger. - } - }, - drag: { - fingers: 3, - position: "relative", // "relative", "absolute", "difference", "move" - listener: function(event, self) { - console.log(self.fingers); // "3" fingers. - console.log(self.x); // coordinate is relative to edge of target. - } - } - } - }); - - * Capturing an event and manually forwarding it to a proxy (tiered events). - ---------------------------------------------------- - Event.add(target, "down", function(event, self) { - var x = event.pageX; // local variables that wont change. - var y = event.pageY; - Event.proxy.drag({ - event: event, - target: target, - listener: function(event, self) { - console.log(x - event.pageX); // measure movement. - console.log(y - event.pageY); - } - }); - }); - ---------------------------------------------------- - - * Event proxies. - * type, fingers, state, start, x, y, position, bbox - * rotation, scale, velocity, angle, delay, timeout - ---------------------------------------------------- - // "Click" :: fingers, minFingers, maxFingers. - Event.add(window, "click", function(event, self) { - console.log(self.gesture, self.x, self.y); - }); - // "Double-Click" :: fingers, minFingers, maxFingers. - Event.add(window, "dblclick", function(event, self) { - console.log(self.gesture, self.x, self.y); - }); - // "Drag" :: fingers, maxFingers, position - Event.add(window, "drag", function(event, self) { - console.log(self.gesture, self.fingers, self.state, self.start, self.x, self.y, self.bbox); - }); - // "Gesture" :: fingers, minFingers, maxFingers. - Event.add(window, "gesture", function(event, self) { - console.log(self.gesture, self.fingers, self.state, self.rotation, self.scale); - }); - // "Swipe" :: fingers, minFingers, maxFingers, snap, threshold. - Event.add(window, "swipe", function(event, self) { - console.log(self.gesture, self.fingers, self.velocity, self.angle, self.start, self.x, self.y); - }); - // "Tap" :: fingers, minFingers, maxFingers, timeout. - Event.add(window, "tap", function(event, self) { - console.log(self.gesture, self.fingers); - }); - // "Longpress" :: fingers, minFingers, maxFingers, delay. - Event.add(window, "longpress", function(event, self) { - console.log(self.gesture, self.fingers); - }); - // - Event.add(window, "shake", function(event, self) { - console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); - }); - // - Event.add(window, "devicemotion", function(event, self) { - console.log(self.gesture, self.acceleration, self.accelerationIncludingGravity); - }); - // - Event.add(window, "wheel", function(event, self) { - console.log(self.gesture, self.state, self.wheelDelta); - }); - - * Stop, prevent and cancel. - ---------------------------------------------------- - Event.stop(event); // stop bubble. - Event.prevent(event); // prevent default. - Event.cancel(event); // stop and prevent. - - * Track for proper command/control-key for Mac/PC. - ---------------------------------------------------- - Event.add(window, "keyup keydown", Event.proxy.metaTracker); - console.log(Event.proxy.metaKey); - - * Test for event features, in this example Drag & Drop file support. - ---------------------------------------------------- - console.log(Event.supports('dragstart') && Event.supports('drop') && !!window.FileReader); - - */ - -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(eventjs) === "undefined") - var eventjs = Event; - -Event = (function(root) { - "use strict"; - -// Add custom *EventListener commands to HTMLElements. - root.modifyEventListener = false; - -// Add bulk *EventListener commands on NodeLists from querySelectorAll and others. - root.modifySelectors = false; +// Add bulk *EventListener commands on NodeLists from querySelectorAll and others (set false to prevent funkiness). +root.modifySelectors = true; // Event maintenance. - root.add = function(target, type, listener, configure) { - return eventManager(target, type, listener, configure, "add"); - }; +root.add = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "add"); +}; - root.remove = function(target, type, listener, configure) { - return eventManager(target, type, listener, configure, "remove"); - }; +root.remove = function(target, type, listener, configure) { + return eventManager(target, type, listener, configure, "remove"); +}; - root.stop = function(event) { - if (event.stopPropagation) - event.stopPropagation(); - event.cancelBubble = true; // <= IE8 - event.bubble = 0; - }; +root.stop = function(event) { + if (!event) return; + if (event.stopPropagation) event.stopPropagation(); + event.cancelBubble = true; // <= IE8 + event.bubble = 0; +}; - root.prevent = function(event) { - if (event.preventDefault) - event.preventDefault(); - event.returnValue = false; // <= IE8 - }; +root.prevent = function(event) { + if (!event) return; + if (event.preventDefault) event.preventDefault(); + if (event.preventManipulation) event.preventManipulation(); // MS + event.returnValue = false; // <= IE8 +}; - root.cancel = function(event) { - root.stop(event); - root.prevent(event); - }; +root.cancel = function(event) { + root.stop(event); + root.prevent(event); +}; // Check whether event is natively supported (via @kangax) - root.supports = function(target, type) { - if (typeof(target) === "string") { - type = target; - target = window; - } - type = "on" + type; - if (type in target) - return true; - if (!target.setAttribute) - target = document.createElement("div"); - if (target.setAttribute && target.removeAttribute) { - target.setAttribute(type, ""); - var isSupported = typeof target[type] === "function"; - if (typeof target[type] !== "undefined") - target[type] = null; - target.removeAttribute(type); - return isSupported; - } - }; +root.getEventSupport = function (target, type) { + if (typeof(target) === "string") { + type = target; + target = window; + } + type = "on" + type; + if (type in target) return true; + if (!target.setAttribute) target = document.createElement("div"); + if (target.setAttribute && target.removeAttribute) { + target.setAttribute(type, ""); + var isSupported = typeof target[type] === "function"; + if (typeof target[type] !== "undefined") target[type] = null; + target.removeAttribute(type); + return isSupported; + } +}; - var clone = function(obj) { - if (!obj || typeof (obj) !== 'object') - return obj; - var temp = new obj.constructor(); - for (var key in obj) { - if (!obj[key] || typeof (obj[key]) !== 'object') { - temp[key] = obj[key]; - } else { // clone sub-object - temp[key] = clone(obj[key]); - } - } - return temp; - }; +var clone = function (obj) { + if (!obj || typeof (obj) !== 'object') return obj; + var temp = new obj.constructor(); + for (var key in obj) { + if (!obj[key] || typeof (obj[key]) !== 'object') { + temp[key] = obj[key]; + } else { // clone sub-object + temp[key] = clone(obj[key]); + } + } + return temp; +}; /// Handle custom *EventListener commands. - var eventManager = function(target, type, listener, configure, trigger, fromOverwrite) { - configure = configure || {}; - // Check for element to load on interval (before onload). - if (typeof(target) === "string" && type === "ready") { - var time = (new Date()).getTime(); - var timeout = configure.timeout; - var ms = configure.interval || 1000 / 60; - var interval = window.setInterval(function() { - if ((new Date()).getTime() - time > timeout) { - window.clearInterval(interval); - } - if (document.querySelector(target)) { - window.clearInterval(interval); - listener(); - } - }, ms); - return; - } - // Get DOM element from Query Selector. - if (typeof(target) === "string") { - target = document.querySelectorAll(target); - if (target.length === 0) - return createError("Missing target on listener!"); // No results. - if (target.length === 1) { // Single target. - target = target[0]; - } - } - /// Handle multiple targets. - var event; - var events = {}; - if (target.length > 0) { - for (var n0 = 0, length0 = target.length; n0 < length0; n0++) { - event = eventManager(target[n0], type, listener, clone(configure), trigger); - if (event) - events[n0] = event; - } - return createBatchCommands(events); - } - // Check for multiple events in one string. - if (type.indexOf && type.indexOf(" ") !== -1) - type = type.split(" "); - if (type.indexOf && type.indexOf(",") !== -1) - type = type.split(","); - // Attach or remove multiple events associated with a target. - if (typeof(type) !== "string") { // Has multiple events. - if (typeof(type.length) === "number") { // Handle multiple listeners glued together. - for (var n1 = 0, length1 = type.length; n1 < length1; n1++) { // Array [type] - event = eventManager(target, type[n1], listener, clone(configure), trigger); - if (event) - events[type[n1]] = event; - } - } else { // Handle multiple listeners. - for (var key in type) { // Object {type} - if (typeof(type[key]) === "function") { // without configuration. - event = eventManager(target, key, type[key], clone(configure), trigger); - } else { // with configuration. - event = eventManager(target, key, type[key].listener, clone(type[key]), trigger); - } - if (event) - events[key] = event; - } - } - return createBatchCommands(events); - } - // Ensure listener is a function. - if (typeof(listener) !== "function") - return createError("Listener is not a function!"); - // Generate a unique wrapper identifier. - var useCapture = configure.useCapture || false; - var id = normalize(type) + getID(target) + "." + getID(listener) + "." + (useCapture ? 1 : 0); - // Handle the event. - if (root.Gesture && root.Gesture._gestureHandlers[type]) { // Fire custom event. - if (trigger === "remove") { // Remove event listener. - if (!wrappers[id]) - return; // Already removed. - wrappers[id].remove(); - delete wrappers[id]; - } else if (trigger === "add") { // Attach event listener. - if (wrappers[id]) - return wrappers[id]; // Already attached. - // Retains "this" orientation. - if (configure.useCall && !root.modifyEventListener) { - var tmp = listener; - listener = function(event, self) { - for (var key in self) - event[key] = self[key]; - return tmp.call(target, event); - }; - } - // Create listener proxy. - configure.gesture = type; - configure.target = target; - configure.listener = listener; - configure.fromOverwrite = fromOverwrite; - // Record wrapper. - wrappers[id] = root.proxy[type](configure); - } - } else { // Fire native event. - type = normalize(type); - if (trigger === "remove") { // Remove event listener. - if (!wrappers[id]) - return; // Already removed. - target[remove](type, listener, useCapture); - delete wrappers[id]; - } else if (trigger === "add") { // Attach event listener. - if (wrappers[id]) - return wrappers[id]; // Already attached. - target[add](type, listener, useCapture); - // Record wrapper. - wrappers[id] = { - type: type, - target: target, - listener: listener, - remove: function() { - root.remove(target, type, listener, configure); - } - }; - } - } - return wrappers[id]; - }; +var eventManager = function(target, type, listener, configure, trigger, fromOverwrite) { + configure = configure || {}; + // Check whether target is a configuration variable; + if (String(target) === "[object Object]") { + var data = target; + target = data.target; + type = data.type; + listener = data.listener; + delete data.target; + delete data.type; + delete data.listener; + for (var key in data) { + configure[key] = data[key]; + } + } + /// + if (!target || !type || !listener) return; + // Check for element to load on interval (before onload). + if (typeof(target) === "string" && type === "ready") { + var time = (new Date()).getTime(); + var timeout = configure.timeout; + var ms = configure.interval || 1000 / 60; + var interval = window.setInterval(function() { + if ((new Date()).getTime() - time > timeout) { + window.clearInterval(interval); + } + if (document.querySelector(target)) { + window.clearInterval(interval); + setTimeout(listener, 1); + } + }, ms); + return; + } + // Get DOM element from Query Selector. + if (typeof(target) === "string") { + target = document.querySelectorAll(target); + if (target.length === 0) return createError("Missing target on listener!", arguments); // No results. + if (target.length === 1) { // Single target. + target = target[0]; + } + } + + /// Handle multiple targets. + var event; + var events = {}; + if (target.length > 0 && target !== window) { + for (var n0 = 0, length0 = target.length; n0 < length0; n0 ++) { + event = eventManager(target[n0], type, listener, clone(configure), trigger); + if (event) events[n0] = event; + } + return createBatchCommands(events); + } + // Check for multiple events in one string. + if (type.indexOf && type.indexOf(" ") !== -1) type = type.split(" "); + if (type.indexOf && type.indexOf(",") !== -1) type = type.split(","); + // Attach or remove multiple events associated with a target. + if (typeof(type) !== "string") { // Has multiple events. + if (typeof(type.length) === "number") { // Handle multiple listeners glued together. + for (var n1 = 0, length1 = type.length; n1 < length1; n1 ++) { // Array [type] + event = eventManager(target, type[n1], listener, clone(configure), trigger); + if (event) events[type[n1]] = event; + } + } else { // Handle multiple listeners. + for (var key in type) { // Object {type} + if (typeof(type[key]) === "function") { // without configuration. + event = eventManager(target, key, type[key], clone(configure), trigger); + } else { // with configuration. + event = eventManager(target, key, type[key].listener, clone(type[key]), trigger); + } + if (event) events[key] = event; + } + } + return createBatchCommands(events); + } + // Ensure listener is a function. + if (typeof(target) !== "object") return createError("Target is not defined!", arguments); + if (typeof(listener) !== "function") return createError("Listener is not a function!", arguments); + // Generate a unique wrapper identifier. + var useCapture = configure.useCapture || false; + var id = getID(target) + "." + getID(listener) + "." + (useCapture ? 1 : 0); + // Handle the event. + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // Fire custom event. + id = type + id; + if (trigger === "remove") { // Remove event listener. + if (!wrappers[id]) return; // Already removed. + wrappers[id].remove(); + delete wrappers[id]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[id]) { + wrappers[id].add(); + return wrappers[id]; // Already attached. + } + // Retains "this" orientation. + if (configure.useCall && !root.modifyEventListener) { + var tmp = listener; + listener = function(event, self) { + for (var key in self) event[key] = self[key]; + return tmp.call(target, event); + }; + } + // Create listener proxy. + configure.gesture = type; + configure.target = target; + configure.listener = listener; + configure.fromOverwrite = fromOverwrite; + // Record wrapper. + wrappers[id] = root.proxy[type](configure); + } + return wrappers[id]; + } else { // Fire native event. + var eventList = getEventList(type); + for (var n = 0, eventId; n < eventList.length; n ++) { + type = eventList[n]; + eventId = type + "." + id; + if (trigger === "remove") { // Remove event listener. + if (!wrappers[eventId]) continue; // Already removed. + target[remove](type, listener, useCapture); + delete wrappers[eventId]; + } else if (trigger === "add") { // Attach event listener. + if (wrappers[eventId]) return wrappers[eventId]; // Already attached. + target[add](type, listener, useCapture); + // Record wrapper. + wrappers[eventId] = { + id: eventId, + type: type, + target: target, + listener: listener, + remove: function() { + for (var n = 0; n < eventList.length; n ++) { + root.remove(target, eventList[n], listener, configure); + } + } + }; + } + } + return wrappers[eventId]; + } +}; /// Perform batch actions on multiple events. - var createBatchCommands = function(events) { - return { - remove: function() { // Remove multiple events. - for (var key in events) { - events[key].remove(); - } - }, - add: function() { // Add multiple events. - for (var key in events) { - events[key].add(); - } - } - }; - }; +var createBatchCommands = function(events) { + return { + remove: function() { // Remove multiple events. + for (var key in events) { + events[key].remove(); + } + }, + add: function() { // Add multiple events. + for (var key in events) { + events[key].add(); + } + } + }; +}; /// Display error message in console. - var createError = function(message) { - if (typeof(console) === "undefined") - return; - if (typeof(console.error) === "undefined") - return; - console.error(message); - }; +var createError = function(message, data) { + if (typeof(console) === "undefined") return; + if (typeof(console.error) === "undefined") return; + console.error(message, data); +}; /// Handle naming discrepancies between platforms. - var normalize = (function() { - var translate = {}; - return function(type) { - if (!root.pointerType) { - if (window.navigator.msPointerEnabled) { - root.pointerType = "mspointer"; - translate = { - "mousedown": "MSPointerDown", - "mousemove": "MSPointerMove", - "mouseup": "MSPointerUp" - }; - } else if (root.supports("touchstart")) { - root.pointerType = "touch"; - translate = { - "mousedown": "touchstart", - "mouseup": "touchend", - "mousemove": "touchmove" - }; - } else { - root.pointerType = "mouse"; - } - } - if (translate[type]) - type = translate[type]; - if (!document.addEventListener) { // IE - return "on" + type; - } else { - return type; - } - }; - })(); +var pointerDefs = { + "msPointer": [ "MSPointerDown", "MSPointerMove", "MSPointerUp" ], + "touch": [ "touchstart", "touchmove", "touchend" ], + "mouse": [ "mousedown", "mousemove", "mouseup" ] +}; + +var pointerDetect = { + // MSPointer + "MSPointerDown": 0, + "MSPointerMove": 1, + "MSPointerUp": 2, + // Touch + "touchstart": 0, + "touchmove": 1, + "touchend": 2, + // Mouse + "mousedown": 0, + "mousemove": 1, + "mouseup": 2 +}; + +var getEventSupport = (function() { + root.supports = {}; + if (window.navigator.msPointerEnabled) { + root.supports.msPointer = true; + } + if (root.getEventSupport("touchstart")) { + root.supports.touch = true; + } + if (root.getEventSupport("mousedown")) { + root.supports.mouse = true; + } +})(); + +var getEventList = (function() { + return function(type) { + var prefix = document.addEventListener ? "" : "on"; // IE + var idx = pointerDetect[type]; + if (isFinite(idx)) { + var types = []; + for (var key in root.supports) { + types.push(prefix + pointerDefs[key][idx]); + } + return types; + } else { + return [ prefix + type ]; + } + }; +})(); /// Event wrappers to keep track of all events placed in the window. - var wrappers = {}; - var counter = 0; - var getID = function(object) { - if (object === window) - return "#window"; - if (object === document) - return "#document"; - if (!object) - return createError("Missing target on listener!"); - if (!object.uniqueID) - object.uniqueID = "id" + counter++; - return object.uniqueID; - }; +var wrappers = {}; +var counter = 0; +var getID = function(object) { + if (object === window) return "#window"; + if (object === document) return "#document"; + if (!object.uniqueID) object.uniqueID = "e" + counter ++; + return object.uniqueID; +}; /// Detect platforms native *EventListener command. - var add = document.addEventListener ? "addEventListener" : "attachEvent"; - var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; +var add = document.addEventListener ? "addEventListener" : "attachEvent"; +var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; - /* - Pointer.js - ------------------------ - Modified from; https://github.com/borismus/pointer.js - */ +/* + Pointer.js + ------------------------ + Modified from; https://github.com/borismus/pointer.js +*/ - root.createPointerEvent = function(event, self, preventRecord) { - var eventName = self.gesture; - var target = self.target; - var pts = event.changedTouches || root.proxy.getCoords(event); - if (pts.length) { - var pt = pts[0]; - self.pointers = preventRecord ? [] : pts; - self.pageX = pt.pageX; - self.pageY = pt.pageY; - self.x = self.pageX; - self.y = self.pageY; - } - /// - var newEvent = document.createEvent("Event"); - newEvent.initEvent(eventName, true, true); - newEvent.originalEvent = event; - for (var k in self) { - if (k === "target") - continue; - newEvent[k] = self[k]; - } - target.dispatchEvent(newEvent); - }; +root.createPointerEvent = function (event, self, preventRecord) { + var eventName = self.gesture; + var target = self.target; + var pts = event.changedTouches || root.proxy.getCoords(event); + if (pts.length) { + var pt = pts[0]; + self.pointers = preventRecord ? [] : pts; + self.pageX = pt.pageX; + self.pageY = pt.pageY; + self.x = self.pageX; + self.y = self.pageY; + } + /// + var newEvent = document.createEvent("Event"); + newEvent.initEvent(eventName, true, true); + newEvent.originalEvent = event; + for (var k in self) { + if (k === "target") continue; + newEvent[k] = self[k]; + } + /// + var type = newEvent.type; + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. +// target.dispatchEvent(newEvent); + self.oldListener.call(target, newEvent, self, false); + } +}; /// Allows *EventListener to use custom event proxies. - if (root.modifyEventListener && window.HTMLElement) - (function() { - var augmentEventListener = function(proto) { - var recall = function(trigger) { // overwrite native *EventListener's - var handle = trigger + "EventListener"; - var handler = proto[handle]; - proto[handle] = function(type, listener, useCapture) { - if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. - var configure = useCapture; - if (typeof(useCapture) === "object") { - configure.useCall = true; - } else { // convert to configuration object. - configure = { - useCall: true, - useCapture: useCapture - }; - } - eventManager(this, type, listener, configure, trigger, true); - handler.call(this, type, listener, useCapture); - } else { // use native function. - handler.call(this, normalize(type), listener, useCapture); - } - }; - }; - recall("add"); - recall("remove"); - }; - // NOTE: overwriting HTMLElement doesn't do anything in Firefox. - if (navigator.userAgent.match(/Firefox/)) { - // TODO: fix Firefox for the general case. - augmentEventListener(HTMLDivElement.prototype); - augmentEventListener(HTMLCanvasElement.prototype); - } else { - augmentEventListener(HTMLElement.prototype); - } - augmentEventListener(document); - augmentEventListener(window); - })(); +if (root.modifyEventListener && window.HTMLElement) (function() { + var augmentEventListener = function(proto) { + var recall = function(trigger) { // overwrite native *EventListener's + var handle = trigger + "EventListener"; + var handler = proto[handle]; + proto[handle] = function (type, listener, useCapture) { + if (root.Gesture && root.Gesture._gestureHandlers[type]) { // capture custom events. + var configure = useCapture; + if (typeof(useCapture) === "object") { + configure.useCall = true; + } else { // convert to configuration object. + configure = { + useCall: true, + useCapture: useCapture + }; + } + eventManager(this, type, listener, configure, trigger, true); +// handler.call(this, type, listener, useCapture); + } else { // use native function. + var types = getEventList(type); + for (var n = 0; n < types.length; n ++) { + handler.call(this, types[n], listener, useCapture); + } + } + }; + }; + recall("add"); + recall("remove"); + }; + // NOTE: overwriting HTMLElement doesn't do anything in Firefox. + if (navigator.userAgent.match(/Firefox/)) { + // TODO: fix Firefox for the general case. + augmentEventListener(HTMLDivElement.prototype); + augmentEventListener(HTMLCanvasElement.prototype); + } else { + augmentEventListener(HTMLElement.prototype); + } + augmentEventListener(document); + augmentEventListener(window); +})(); /// Allows querySelectorAll and other NodeLists to perform *EventListener commands in bulk. - if (root.modifySelectors) - (function() { - var proto = NodeList.prototype; - proto.removeEventListener = function(type, listener, useCapture) { - for (var n = 0, length = this.length; n < length; n++) { - this[n].removeEventListener(type, listener, useCapture); - } - }; - proto.addEventListener = function(type, listener, useCapture) { - for (var n = 0, length = this.length; n < length; n++) { - this[n].addEventListener(type, listener, useCapture); - } - }; - })(); +if (root.modifySelectors) (function() { + var proto = NodeList.prototype; + proto.removeEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n ++) { + this[n].removeEventListener(type, listener, useCapture); + } + }; + proto.addEventListener = function(type, listener, useCapture) { + for (var n = 0, length = this.length; n < length; n ++) { + this[n].addEventListener(type, listener, useCapture); + } + }; +})(); - return root; +return root; })(Event); /* - ---------------------------------------------------- - Event.proxy : 0.4.2 : 2012/07/29 : MIT License - ---------------------------------------------------- - https://github.com/mudcube/Event.js - ---------------------------------------------------- - Pointer Gestures - ---------------------------------------------------- - 1 : click, dblclick, dbltap - 1+ : tap, taphold, drag, swipe - 2+ : pinch, rotate - ---------------------------------------------------- - Gyroscope Gestures - ---------------------------------------------------- - * shake - ---------------------------------------------------- - Fixes issues with - ---------------------------------------------------- - * mousewheel-Firefox uses DOMMouseScroll and does not return wheelDelta. - * devicemotion-Fixes issue where event.acceleration is not returned. - ---------------------------------------------------- - Ideas for the future - ---------------------------------------------------- - * Keyboard, GamePad, and other input abstractions. - * Event batching - i.e. for every x fingers down a new gesture is created. - */ + ---------------------------------------------------- + Event.proxy : 0.4.3 : 2013/07/17 : MIT License + ---------------------------------------------------- + https://github.com/mudcube/Event.js + ---------------------------------------------------- +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - /* - Create a new pointer gesture instance. - */ +/* + Create a new pointer gesture instance. +*/ - root.pointerSetup = function(conf, self) { - /// Configure. - conf.doc = conf.target.ownerDocument || conf.target; // Associated document. - conf.minFingers = conf.minFingers || conf.fingers || 1; // Minimum required fingers. - conf.maxFingers = conf.maxFingers || conf.fingers || Infinity; // Maximum allowed fingers. - conf.position = conf.position || "relative"; // Determines what coordinate system points are returned. - delete conf.fingers; //- - /// Convenience data. - self = self || {}; - self.gesture = conf.gesture; - self.target = conf.target; - self.pointerType = Event.pointerType; - /// - if (Event.modifyEventListener && conf.fromOverwrite) - conf.listener = Event.createPointerEvent; - /// Convenience commands. - var fingers = 0; - var type = self.gesture.indexOf("pointer") === 0 && Event.modifyEventListener ? "pointer" : "mouse"; - self.listener = conf.listener; - self.proxy = function(listener) { - self.defaultListener = conf.listener; - conf.listener = listener; - listener(conf.event, self); - }; - self.remove = function() { - if (conf.onPointerDown) - Event.remove(conf.target, type + "down", conf.onPointerDown); - if (conf.onPointerMove) - Event.remove(conf.doc, type + "move", conf.onPointerMove); - if (conf.onPointerUp) - Event.remove(conf.doc, type + "up", conf.onPointerUp); - }; - self.resume = function(opt) { - if (conf.onPointerMove && (!opt || opt.move)) - Event.add(conf.doc, type + "move", conf.onPointerMove); - if (conf.onPointerUp && (!opt || opt.move)) - Event.add(conf.doc, type + "up", conf.onPointerUp); - conf.fingers = fingers; - }; - self.pause = function(opt) { - fingers = conf.fingers; - if (conf.onPointerMove && (!opt || opt.move)) - Event.remove(conf.doc, type + "move", conf.onPointerMove); - if (conf.onPointerUp && (!opt || opt.up)) - Event.remove(conf.doc, type + "up", conf.onPointerUp); - conf.fingers = 0; - }; - /// - return self; - }; +root.pointerSetup = function(conf, self) { + /// Configure. + conf.doc = conf.target.ownerDocument || conf.target; // Associated document. + conf.minFingers = conf.minFingers || conf.fingers || 1; // Minimum required fingers. + conf.maxFingers = conf.maxFingers || conf.fingers || Infinity; // Maximum allowed fingers. + conf.position = conf.position || "relative"; // Determines what coordinate system points are returned. + delete conf.fingers; //- + /// Convenience data. + self = self || {}; + self.enabled = true; + self.gesture = conf.gesture; + self.target = conf.target; + self.env = conf.env; + /// + if (Event.modifyEventListener && conf.fromOverwrite) { + conf.oldListener = conf.listener; + conf.listener = Event.createPointerEvent; + } + /// Convenience commands. + var fingers = 0; + var type = self.gesture.indexOf("pointer") === 0 && Event.modifyEventListener ? "pointer" : "mouse"; + if (conf.oldListener) self.oldListener = conf.oldListener; + self.listener = conf.listener; + self.proxy = function(listener) { + self.defaultListener = conf.listener; + conf.listener = listener; + listener(conf.event, self); + }; + self.add = function() { + if (self.enabled === true) return; + if (conf.onPointerDown) Event.add(conf.target, type + "down", conf.onPointerDown); + if (conf.onPointerMove) Event.add(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp) Event.add(conf.doc, type + "up", conf.onPointerUp); + self.enabled = true; + }; + self.remove = function() { + if (self.enabled === false) return; + if (conf.onPointerDown) Event.remove(conf.target, type + "down", conf.onPointerDown); + if (conf.onPointerMove) Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp) Event.remove(conf.doc, type + "up", conf.onPointerUp); + self.reset(); + self.enabled = false; + }; + self.pause = function(opt) { + if (conf.onPointerMove && (!opt || opt.move)) Event.remove(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.up)) Event.remove(conf.doc, type + "up", conf.onPointerUp); + fingers = conf.fingers; + conf.fingers = 0; + }; + self.resume = function(opt) { + if (conf.onPointerMove && (!opt || opt.move)) Event.add(conf.doc, type + "move", conf.onPointerMove); + if (conf.onPointerUp && (!opt || opt.up)) Event.add(conf.doc, type + "up", conf.onPointerUp); + conf.fingers = fingers; + }; + self.reset = function() { + conf.tracker = {}; + conf.fingers = 0; + }; + /// + return self; +}; - /* - Begin proxied pointer command. - */ +/* + Begin proxied pointer command. +*/ - root.pointerStart = function(event, self, conf) { - var addTouchStart = function(touch, sid) { - var bbox = conf.bbox; - var pt = track[sid] = {}; - /// - switch (conf.position) { - case "absolute": // Absolute from within window. - pt.offsetX = 0; - pt.offsetY = 0; - break; - case "difference": // Relative from origin. - pt.offsetX = touch.pageX; - pt.offsetY = touch.pageY; - break; - case "move": // Move target element. - pt.offsetX = touch.pageX - bbox.x1; - pt.offsetY = touch.pageY - bbox.y1; - break; - default: // Relative from within target. - pt.offsetX = bbox.x1; - pt.offsetY = bbox.y1; - break; - } - /// - if (conf.position === "relative") { - var x = (touch.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; - var y = (touch.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; - } else { - var x = (touch.pageX - pt.offsetX); - var y = (touch.pageY - pt.offsetY); - } - /// - pt.rotation = 0; - pt.scale = 1; - pt.startTime = pt.moveTime = (new Date).getTime(); - pt.move = {x: x, y: y}; - pt.start = {x: x, y: y}; - /// - conf.fingers++; - }; - /// - conf.event = event; - if (self.defaultListener) { - conf.listener = self.defaultListener; - delete self.defaultListener; - } - /// - var isTouchStart = !conf.fingers; - var track = conf.tracker; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - // Adding touch events to tracking. - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; // Touch ID. - // Track the current state of the touches. - if (conf.fingers) { - if (conf.fingers >= conf.maxFingers) { - var ids = []; - for (var sid in conf.tracker) - ids.push(sid); - self.identifier = ids.join(","); - return isTouchStart; - } - var fingers = 0; // Finger ID. - for (var rid in track) { - // Replace removed finger. - if (track[rid].up) { - delete track[rid]; - addTouchStart(touch, sid); - conf.cancel = true; - break; - } - fingers++; - } - // Add additional finger. - if (track[sid]) - continue; - addTouchStart(touch, sid); - } else { // Start tracking fingers. - track = conf.tracker = {}; - self.bbox = conf.bbox = root.getBoundingBox(conf.target); - conf.fingers = 0; - conf.cancel = false; - addTouchStart(touch, sid); - } - } - /// - var ids = []; - for (var sid in conf.tracker) - ids.push(sid); - self.identifier = ids.join(","); - /// - return isTouchStart; - }; +var sp = Event.supports; +Event.pointerType = sp.mouse ? "mouse" : sp.touch ? "touch" : "mspointer"; +root.pointerStart = function(event, self, conf) { + var type = (event.type || "mousedown").toUpperCase(); + if (type.indexOf("MOUSE") === 0) Event.pointerType = "mouse"; + else if (type.indexOf("TOUCH") === 0) Event.pointerType = "touch"; + else if (type.indexOf("MSPOINTER") === 0) Event.pointerType = "mspointer"; + /// + var addTouchStart = function(touch, sid) { + var bbox = conf.bbox; + var pt = track[sid] = {}; + /// + switch(conf.position) { + case "absolute": // Absolute from within window. + pt.offsetX = 0; + pt.offsetY = 0; + break; + case "differenceFromLast": // Since last coordinate recorded. + pt.offsetX = touch.pageX; + pt.offsetY = touch.pageY; + break; + case "difference": // Relative from origin. + pt.offsetX = touch.pageX; + pt.offsetY = touch.pageY; + break; + case "move": // Move target element. + pt.offsetX = touch.pageX - bbox.x1; + pt.offsetY = touch.pageY - bbox.y1; + break; + default: // Relative from within target. + pt.offsetX = bbox.x1; + pt.offsetY = bbox.y1; + break; + } + /// + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - pt.offsetX); + var y = (touch.pageY + bbox.scrollTop - pt.offsetY); + } else { + var x = (touch.pageX - pt.offsetX); + var y = (touch.pageY - pt.offsetY); + } + /// + pt.rotation = 0; + pt.scale = 1; + pt.startTime = pt.moveTime = (new Date()).getTime(); + pt.move = { x: x, y: y }; + pt.start = { x: x, y: y }; + /// + conf.fingers ++; + }; + /// + conf.event = event; + if (self.defaultListener) { + conf.listener = self.defaultListener; + delete self.defaultListener; + } + /// + var isTouchStart = !conf.fingers; + var track = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Adding touch events to tracking. + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; // Touch ID. + // Track the current state of the touches. + if (conf.fingers) { + if (conf.fingers >= conf.maxFingers) { + var ids = []; + for (var sid in conf.tracker) ids.push(sid); + self.identifier = ids.join(","); + return isTouchStart; + } + var fingers = 0; // Finger ID. + for (var rid in track) { + // Replace removed finger. + if (track[rid].up) { + delete track[rid]; + addTouchStart(touch, sid); + conf.cancel = true; + break; + } + fingers ++; + } + // Add additional finger. + if (track[sid]) continue; + addTouchStart(touch, sid); + } else { // Start tracking fingers. + track = conf.tracker = {}; + self.bbox = conf.bbox = root.getBoundingBox(conf.target); + conf.fingers = 0; + conf.cancel = false; + addTouchStart(touch, sid); + } + } + /// + var ids = []; + for (var sid in conf.tracker) ids.push(sid); + self.identifier = ids.join(","); + /// + return isTouchStart; +}; - /* - End proxied pointer command. - */ +/* + End proxied pointer command. +*/ - root.pointerEnd = function(event, self, conf, onPointerUp) { - // Record changed touches have ended (iOS changedTouches is not reliable). - var touches = event.touches || []; - var length = touches.length; - var exists = {}; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier; - exists[sid || Infinity] = true; - } - for (var sid in conf.tracker) { - var track = conf.tracker[sid]; - if (exists[sid] || track.up) - continue; - if (onPointerUp) { // add changedTouches to mouse. - onPointerUp({ - pageX: track.pageX, - pageY: track.pageY, - changedTouches: [{ - pageX: track.pageX, - pageY: track.pageY, - identifier: sid === "Infinity" ? Infinity : sid - }] - }, "up"); - } - track.up = true; - conf.fingers--; - } - /* // This should work but fails in Safari on iOS4 so not using it. - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - // Record changed touches have ended (this should work). - for (var i = 0; i < length; i ++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; - var track = conf.tracker[sid]; - if (track && !track.up) { - if (onPointerUp) { // add changedTouches to mouse. - onPointerUp({ - changedTouches: [{ - pageX: track.pageX, - pageY: track.pageY, - identifier: sid === "Infinity" ? Infinity : sid - }] - }, "up"); - } - track.up = true; - conf.fingers --; - } - } */ - // Wait for all fingers to be released. - if (conf.fingers !== 0) - return false; - // Record total number of fingers gesture used. - var ids = []; - conf.gestureFingers = 0; - for (var sid in conf.tracker) { - conf.gestureFingers++; - ids.push(sid); - } - self.identifier = ids.join(","); - // Our pointer gesture has ended. - return true; - }; +root.pointerEnd = function(event, self, conf, onPointerUp) { + // Record changed touches have ended (iOS changedTouches is not reliable). + var touches = event.touches || []; + var length = touches.length; + var exists = {}; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier; + exists[sid || Infinity] = true; + } + for (var sid in conf.tracker) { + var track = conf.tracker[sid]; + if (exists[sid] || track.up) continue; + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + pageX: track.pageX, + pageY: track.pageY, + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers --; + } +/* // This should work but fails in Safari on iOS4 so not using it. + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Record changed touches have ended (this should work). + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var track = conf.tracker[sid]; + if (track && !track.up) { + if (onPointerUp) { // add changedTouches to mouse. + onPointerUp({ + changedTouches: [{ + pageX: track.pageX, + pageY: track.pageY, + identifier: sid === "Infinity" ? Infinity : sid + }] + }, "up"); + } + track.up = true; + conf.fingers --; + } + } */ + // Wait for all fingers to be released. + if (conf.fingers !== 0) return false; + // Record total number of fingers gesture used. + var ids = []; + conf.gestureFingers = 0; + for (var sid in conf.tracker) { + conf.gestureFingers ++; + ids.push(sid); + } + self.identifier = ids.join(","); + // Our pointer gesture has ended. + return true; +}; - /* - Returns mouse coords in an array to match event.*Touches - ------------------------------------------------------------ - var touch = event.changedTouches || root.getCoords(event); - */ +/* + Returns mouse coords in an array to match event.*Touches + ------------------------------------------------------------ + var touch = event.changedTouches || root.getCoords(event); +*/ - root.getCoords = function(event) { - if (typeof(event.pageX) !== "undefined") { // Desktop browsers. - root.getCoords = function(event) { - return Array({ - type: "mouse", - x: event.pageX, - y: event.pageY, - pageX: event.pageX, - pageY: event.pageY, - identifier: Infinity - }); - }; - } else { // Internet Explorer <= 8.0 - root.getCoords = function(event) { - event = event || window.event; - return Array({ - type: "mouse", - x: event.clientX + document.documentElement.scrollLeft, - y: event.clientY + document.documentElement.scrollTop, - pageX: event.clientX + document.documentElement.scrollLeft, - pageY: event.clientY + document.documentElement.scrollTop, - identifier: Infinity - }); - }; - } - return root.getCoords(event); - }; +root.getCoords = function(event) { + if (typeof(event.pageX) !== "undefined") { // Desktop browsers. + root.getCoords = function(event) { + return Array({ + type: "mouse", + x: event.pageX, + y: event.pageY, + pageX: event.pageX, + pageY: event.pageY, + identifier: event.pointerId || Infinity // pointerId is MS + }); + }; + } else { // Internet Explorer <= 8.0 + root.getCoords = function(event) { + event = event || window.event; + return Array({ + type: "mouse", + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop, + pageX: event.clientX + document.documentElement.scrollLeft, + pageY: event.clientY + document.documentElement.scrollTop, + identifier: Infinity + }); + }; + } + return root.getCoords(event); +}; - /* - Returns single coords in an object. - ------------------------------------------------------------ - var mouse = root.getCoord(event); - */ +/* + Returns single coords in an object. + ------------------------------------------------------------ + var mouse = root.getCoord(event); +*/ - root.getCoord = function(event) { - if ("ontouchstart" in window) { // Mobile browsers. - var pX = 0; - var pY = 0; - root.getCoord = function(event) { - var touches = event.changedTouches; - if (touches.length) { // ontouchstart + ontouchmove - return { - x: pX = touches[0].pageX, - y: pY = touches[0].pageY - }; - } else { // ontouchend - return { - x: pX, - y: pY - }; - } - }; - } else if (typeof(event.pageX) !== "undefined" && typeof(event.pageY) !== "undefined") { // Desktop browsers. - root.getCoord = function(event) { - return { - x: event.pageX, - y: event.pageY - }; - }; - } else { // Internet Explorer <=8.0 - root.getCoord = function(event) { - event = event || window.event; - return { - x: event.clientX + document.documentElement.scrollLeft, - y: event.clientY + document.documentElement.scrollTop - }; - }; - } - return root.getCoord(event); - }; +root.getCoord = function(event) { + if ("ontouchstart" in window) { // Mobile browsers. + var pX = 0; + var pY = 0; + root.getCoord = function(event) { + var touches = event.changedTouches; + if (touches && touches.length) { // ontouchstart + ontouchmove + return { + x: pX = touches[0].pageX, + y: pY = touches[0].pageY + }; + } else { // ontouchend + return { + x: pX, + y: pY + }; + } + }; + } else if(typeof(event.pageX) !== "undefined" && typeof(event.pageY) !== "undefined") { // Desktop browsers. + root.getCoord = function(event) { + return { + x: event.pageX, + y: event.pageY + }; + }; + } else { // Internet Explorer <=8.0 + root.getCoord = function(event) { + event = event || window.event; + return { + x: event.clientX + document.documentElement.scrollLeft, + y: event.clientY + document.documentElement.scrollTop + }; + }; + } + return root.getCoord(event); +}; - /* - Get target scale and position in space. - */ +/* + Get target scale and position in space. +*/ - root.getBoundingBox = function(o) { - if (o === window || o === document) - o = document.body; - /// - var bbox = { - x1: 0, - y1: 0, - x2: 0, - y2: 0, - scrollLeft: 0, - scrollTop: 0 - }; - /// - if (o === document.body) { - bbox.height = window.innerHeight; - bbox.width = window.innerWidth; - } else { - bbox.height = o.offsetHeight; - bbox.width = o.offsetWidth; - } - /// Get the scale of the element. - bbox.scaleX = o.width / bbox.width || 1; - bbox.scaleY = o.height / bbox.height || 1; - /// Get the offset of element. - var tmp = o; - while (tmp !== null) { - bbox.x1 += tmp.offsetLeft; - bbox.y1 += tmp.offsetTop; - tmp = tmp.offsetParent; - } - ; - /// Get the scroll of container element. - var tmp = o.parentNode; - while (tmp !== null) { - if (tmp === document.body) - break; - if (tmp.scrollTop === undefined) - break; - bbox.scrollLeft += tmp.scrollLeft; - bbox.scrollTop += tmp.scrollTop; - tmp = tmp.parentNode; - } - ; - /// Record the extent of box. - bbox.x2 = bbox.x1 + bbox.width; - bbox.y2 = bbox.y1 + bbox.height; - /// - return bbox; - }; +root.getBoundingBox = function(o) { + if (o === window || o === document) o = document.body; + /// + var bbox = {}; + var bcr = o.getBoundingClientRect(); + bbox.width = bcr.width; + bbox.height = bcr.height; + bbox.x1 = bcr.left; + bbox.y1 = bcr.top; + bbox.x2 = bbox.x1 + bbox.width; + bbox.y2 = bbox.y1 + bbox.height; + bbox.scaleX = bcr.width / o.offsetWidth || 1; + bbox.scaleY = bcr.height / o.offsetHeight || 1; + bbox.scrollLeft = 0; + bbox.scrollTop = 0; - /* - Keep track of metaKey, the proper ctrlKey for users platform. - */ + /// Get the scroll of container element. + var tmp = o.parentNode; + while (tmp !== null) { + if (tmp === document.body) break; + if (tmp.scrollTop === undefined) break; + var style = window.getComputedStyle(tmp); + var position = style.getPropertyValue("position"); + if (position === "absolute") { + break; + } else if (position === "fixed") { + bbox.scrollTop -= tmp.parentNode.scrollTop; + break; + } else { + bbox.scrollLeft += tmp.scrollLeft; + bbox.scrollTop += tmp.scrollTop; + } + tmp = tmp.parentNode; + }; + /// + return bbox; +}; - (function() { - var agent = navigator.userAgent.toLowerCase(); - var mac = agent.indexOf("macintosh") !== -1; - if (mac && agent.indexOf("khtml") !== -1) { // chrome, safari. - var watch = {91: true, 93: true}; - } else if (mac && agent.indexOf("firefox") !== -1) { // mac firefox. - var watch = {224: true}; - } else { // windows, linux, or mac opera. - var watch = {17: true}; - } - root.isMetaKey = function(event) { - return !!watch[event.keyCode]; - }; - root.metaTracker = function(event) { - if (watch[event.keyCode]) { - root.metaKey = event.type === "keydown"; - } - }; - })(); +/* + Keep track of metaKey, the proper ctrlKey for users platform. +*/ - return root; +(function() { + var agent = navigator.userAgent.toLowerCase(); + var mac = agent.indexOf("macintosh") !== -1; + if (mac && agent.indexOf("khtml") !== -1) { // chrome, safari. + var watch = { 91: true, 93: true }; + } else if (mac && agent.indexOf("firefox") !== -1) { // mac firefox. + var watch = { 224: true }; + } else { // windows, linux, or mac opera. + var watch = { 17: true }; + } + root.metaTrackerReset = function() { + root.metaKey = false; + root.ctrlKey = false; + root.shiftKey = false; + root.altKey = false; + }; + root.metaTracker = function(event) { + var check = !!watch[event.keyCode]; + if (check) root.metaKey = event.type === "keydown"; + root.ctrlKey = event.ctrlKey; + root.shiftKey = event.shiftKey; + root.altKey = event.altKey; + return check; + }; +})(); + +return root; })(Event.proxy); /* - "Click" event proxy. - ---------------------------------------------------- - Event.add(window, "click", function(event, self) {}); - */ + ---------------------------------------------------- + "MutationObserver" event proxy. + ---------------------------------------------------- + Author: Selvakumar Arumugam (MIT LICENSE) + http://stackoverflow.com/questions/10868104/can-you-have-a-javascript-hook-trigger-after-a-dom-elements-style-object-change + ---------------------------------------------------- +*/ +if (typeof(Event) === "undefined") var Event = {}; -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +Event.MutationObserver = (function() { + var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; + var DOMAttrModifiedSupported = (function() { + var p = document.createElement("p"); + var flag = false; + var fn = function() { flag = true }; + if (p.addEventListener) { + p.addEventListener("DOMAttrModified", fn, false); + } else if (p.attachEvent) { + p.attachEvent("onDOMAttrModified", fn); + } else { + return false; + } + /// + p.setAttribute("id", "target"); + /// + return flag; + })(); + /// + return function(container, callback) { + if (MutationObserver) { + var options = { + subtree: false, + attributes: true + }; + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(e) { + callback.call(e.target, e.attributeName); + }); + }); + observer.observe(container, options) + } else if (DOMAttrModifiedSupported) { + Event.add(container, "DOMAttrModified", function(e) { + callback.call(container, e.attrName); + }); + } else if ("onpropertychange" in document.body) { + Event.add(container, "propertychange", function(e) { + callback.call(container, window.event.propertyName); + }); + } + } +})(); +/* + "Click" event proxy. + ---------------------------------------------------- + Event.add(window, "click", function(event, self) {}); +*/ -Event.proxy = (function(root) { - "use strict"; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; - root.click = function(conf) { - conf.maxFingers = conf.maxFingers || conf.fingers || 1; - // Setting up local variables. - var EVENT; - // Tracking the events. - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - }; - conf.onPointerMove = function(event) { - EVENT = event; - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - if (EVENT.cancelBubble && ++EVENT.bubble > 1) - return; - var pointers = EVENT.changedTouches || root.getCoords(EVENT); - var pointer = pointers[0]; - var bbox = conf.bbox; - var newbbox = root.getBoundingBox(conf.target); - if (conf.position === "relative") { - var ax = (pointer.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - var ay = (pointer.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - var ax = (pointer.pageX - bbox.x1); - var ay = (pointer.pageY - bbox.y1); - } - if (ax > 0 && ax < bbox.width && // Within target coordinates. - ay > 0 && ay < bbox.height && - bbox.scrollTop === newbbox.scrollTop) { - conf.listener(EVENT, self); - } - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - self.state = "click"; - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +Event.proxy = (function(root) { "use strict"; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.click = root.click; +root.click = function(conf) { + conf.gesture = conf.gesture || "click"; + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var EVENT; + // Tracking the events. + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function (event) { + EVENT = event; + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (EVENT.cancelBubble && ++ EVENT.bubble > 1) return; + var pointers = EVENT.changedTouches || root.getCoords(EVENT); + var pointer = pointers[0]; + var bbox = conf.bbox; + var newbbox = root.getBoundingBox(conf.target); + if (conf.position === "relative") { + var ax = (pointer.pageX + bbox.scrollLeft - bbox.x1); + var ay = (pointer.pageY + bbox.scrollTop - bbox.y1); + } else { + var ax = (pointer.pageX - bbox.x1); + var ay = (pointer.pageY - bbox.y1); + } + if (ax > 0 && ax < bbox.width && // Within target coordinates. + ay > 0 && ay < bbox.height && + bbox.scrollTop === newbbox.scrollTop) { + /// + for (var key in conf.tracker) break; //- should be modularized? in dblclick too + var point = conf.tracker[key]; + self.x = point.start.x; + self.y = point.start.y; + /// + conf.listener(EVENT, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "click"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - return root; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.click = root.click; + +return root; })(Event.proxy); /* - "Double-Click" aka "Double-Tap" event proxy. - ---------------------------------------------------- - Event.add(window, "dblclick", function(event, self) {}); - ---------------------------------------------------- - Touch an target twice for <= 700ms, with less than 25 pixel drift. - */ + "Double-Click" aka "Double-Tap" event proxy. + ---------------------------------------------------- + Event.add(window, "dblclick", function(event, self) {}); + ---------------------------------------------------- + Touch an target twice for <= 700ms, with less than 25 pixel drift. +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.dbltap = - root.dblclick = function(conf) { - conf.maxFingers = conf.maxFingers || conf.fingers || 1; - // Setting up local variables. - var delay = 700; // in milliseconds - var time0, time1, timeout; - var pointer0, pointer1; - // Tracking the events. - conf.onPointerDown = function(event) { - var pointers = event.changedTouches || root.getCoords(event); - if (time0 && !time1) { // Click #2 - pointer1 = pointers[0]; - time1 = (new Date).getTime() - time0; - } else { // Click #1 - pointer0 = pointers[0]; - time0 = (new Date).getTime(); - time1 = 0; - clearTimeout(timeout); - timeout = setTimeout(function() { - time0 = 0; - }, delay); - } - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - }; - conf.onPointerMove = function(event) { - if (time0 && !time1) { - var pointers = event.changedTouches || root.getCoords(event); - pointer1 = pointers[0]; - } - var bbox = conf.bbox; - if (conf.position === "relative") { - var ax = (pointer1.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - var ay = (pointer1.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - var ax = (pointer1.pageX - bbox.x1); - var ay = (pointer1.pageY - bbox.y1); - } - if (!(ax > 0 && ax < bbox.width && // Within target coordinates.. - ay > 0 && ay < bbox.height && - Math.abs(pointer1.pageX - pointer0.pageX) <= 25 && // Within drift deviance. - Math.abs(pointer1.pageY - pointer0.pageY) <= 25)) { - // Cancel out this listener. - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - clearTimeout(timeout); - time0 = time1 = 0; - } - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - } - if (time0 && time1) { - if (time1 <= delay && !(event.cancelBubble && ++event.bubble > 1)) { - self.state = conf.gesture; - conf.listener(event, self); - } - clearTimeout(timeout); - time0 = time1 = 0; - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - self.state = "dblclick"; - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +root.dbltap = +root.dblclick = function(conf) { + conf.gesture = conf.gesture || "dbltap"; + conf.maxFingers = conf.maxFingers || conf.fingers || 1; + // Setting up local variables. + var delay = 700; // in milliseconds + var time0, time1, timeout; + var pointer0, pointer1; + // Tracking the events. + conf.onPointerDown = function (event) { + var pointers = event.changedTouches || root.getCoords(event); + if (time0 && !time1) { // Click #2 + pointer1 = pointers[0]; + time1 = (new Date()).getTime() - time0; + } else { // Click #1 + pointer0 = pointers[0]; + time0 = (new Date()).getTime(); + time1 = 0; + clearTimeout(timeout); + timeout = setTimeout(function() { + time0 = 0; + }, delay); + } + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function (event) { + if (time0 && !time1) { + var pointers = event.changedTouches || root.getCoords(event); + pointer1 = pointers[0]; + } + var bbox = conf.bbox; + if (conf.position === "relative") { + var ax = (pointer1.pageX + bbox.scrollLeft - bbox.x1); + var ay = (pointer1.pageY + bbox.scrollTop - bbox.y1); + } else { + var ax = (pointer1.pageX - bbox.x1); + var ay = (pointer1.pageY - bbox.y1); + } + if (!(ax > 0 && ax < bbox.width && // Within target coordinates.. + ay > 0 && ay < bbox.height && + Math.abs(pointer1.pageX - pointer0.pageX) <= 25 && // Within drift deviance. + Math.abs(pointer1.pageY - pointer0.pageY) <= 25)) { + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + if (time0 && time1) { + if (time1 <= delay && !(event.cancelBubble && ++event.bubble > 1)) { + self.state = conf.gesture; + for (var key in conf.tracker) break; + var point = conf.tracker[key]; + self.x = point.start.x; + self.y = point.start.y; + conf.listener(event, self); + } + clearTimeout(timeout); + time0 = time1 = 0; + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + self.state = "dblclick"; + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.dbltap = root.dbltap; - Event.Gesture._gestureHandlers.dblclick = root.dblclick; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.dbltap = root.dbltap; +Event.Gesture._gestureHandlers.dblclick = root.dblclick; - return root; +return root; })(Event.proxy); /* - "Drag" event proxy (1+ fingers). - ---------------------------------------------------- - CONFIGURE: maxFingers, position. - ---------------------------------------------------- - Event.add(window, "drag", function(event, self) { - console.log(self.gesture, self.state, self.start, self.x, self.y, self.bbox); - }); - */ + "Drag" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: maxFingers, position. + ---------------------------------------------------- + Event.add(window, "drag", function(event, self) { + console.log(self.gesture, self.state, self.start, self.x, self.y, self.bbox); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.dragElement = function(that, event) { - root.drag({ - event: event, - target: that, - position: "move", - listener: function(event, self) { - that.style.left = self.x + "px"; - that.style.top = self.y + "px"; - Event.prevent(event); - } - }); - }; +root.dragElement = function(that, event) { + root.drag({ + event: event, + target: that, + position: "move", + listener: function(event, self) { + that.style.left = self.x + "px"; + that.style.top = self.y + "px"; + Event.prevent(event); + } + }); +}; - root.drag = function(conf) { - conf.gesture = "drag"; - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - if (!conf.monitor) { - Event.add(conf.doc, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - } - // Process event listener. - conf.onPointerMove(event, "down"); - }; - conf.onPointerMove = function(event, state) { - if (!conf.tracker) - return conf.onPointerDown(event); - var bbox = conf.bbox; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var identifier = touch.identifier || Infinity; - var pt = conf.tracker[identifier]; - // Identifier defined outside of listener. - if (!pt) - continue; - pt.pageX = touch.pageX; - pt.pageY = touch.pageY; - // Record data. - self.state = state || "move"; - self.identifier = identifier; - self.start = pt.start; - self.fingers = conf.fingers; - if (conf.position === "relative") { - self.x = (pt.pageX + bbox.scrollLeft - pt.offsetX) * bbox.scaleX; - self.y = (pt.pageY + bbox.scrollTop - pt.offsetY) * bbox.scaleY; - } else { - self.x = (pt.pageX - pt.offsetX); - self.y = (pt.pageY - pt.offsetY); - } - /// - conf.listener(event, self); - } - }; - conf.onPointerUp = function(event) { - // Remove tracking for touch. - if (root.pointerEnd(event, self, conf, conf.onPointerMove)) { - if (!conf.monitor) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - } - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - if (conf.event) { - conf.onPointerDown(conf.event); - } else { // - Event.add(conf.target, "mousedown", conf.onPointerDown); - if (conf.monitor) { - Event.add(conf.doc, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - } - // Return this object. - return self; - }; +root.drag = function(conf) { + conf.gesture = "drag"; + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + if (!conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Process event listener. + conf.onPointerMove(event, "down"); + }; + conf.onPointerMove = function (event, state) { + if (!conf.tracker) return conf.onPointerDown(event); + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + // Identifier defined outside of listener. + if (!pt) continue; + pt.pageX = touch.pageX; + pt.pageY = touch.pageY; + // Record data. + self.state = state || "move"; + self.identifier = identifier; + self.start = pt.start; + self.fingers = conf.fingers; + if (conf.position === "differenceFromLast") { + self.x = (pt.pageX - pt.offsetX); + self.y = (pt.pageY - pt.offsetY); + pt.offsetX = pt.pageX; + pt.offsetY = pt.pageY; + } else if (conf.position === "relative") { + self.x = (pt.pageX + bbox.scrollLeft - pt.offsetX); + self.y = (pt.pageY + bbox.scrollTop - pt.offsetY); + } else { + self.x = (pt.pageX - pt.offsetX); + self.y = (pt.pageY - pt.offsetY); + } + /// + conf.listener(event, self); + } + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + if (root.pointerEnd(event, self, conf, conf.onPointerMove)) { + if (!conf.monitor) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + if (conf.event) { + conf.onPointerDown(conf.event); + } else { // + Event.add(conf.target, "mousedown", conf.onPointerDown); + if (conf.monitor) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + } + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.drag = root.drag; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.drag = root.drag; - return root; +return root; })(Event.proxy); /* - "Gesture" event proxy (2+ fingers). - ---------------------------------------------------- - CONFIGURE: minFingers, maxFingers. - ---------------------------------------------------- - Event.add(window, "gesture", function(event, self) { - console.log(self.rotation, self.scale, self.fingers, self.state); - }); - */ + "Gesture" event proxy (2+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - var RAD_DEG = Math.PI / 180; +var RAD_DEG = Math.PI / 180; - root.gesture = function(conf) { - conf.minFingers = conf.minFingers || conf.fingers || 2; - // Tracking the events. - conf.onPointerDown = function(event) { - var fingers = conf.fingers; - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - // Record gesture start. - if (conf.fingers === conf.minFingers && fingers !== conf.fingers) { - self.fingers = conf.minFingers; - self.scale = 1; - self.rotation = 0; - self.state = "start"; - var sids = ""; //- FIXME(mud): can generate duplicate IDs. - for (var key in conf.tracker) - sids += key; - self.identifier = parseInt(sids); - conf.listener(event, self); - } - }; - /// - conf.onPointerMove = function(event, state) { - var bbox = conf.bbox; - var points = conf.tracker; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - // Update tracker coordinates. - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; - var pt = points[sid]; - // Check whether "pt" is used by another gesture. - if (!pt) - continue; - // Find the actual coordinates. - if (conf.position === "relative") { - pt.move.x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - pt.move.y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - pt.move.x = (touch.pageX - bbox.x1); - pt.move.y = (touch.pageY - bbox.y1); - } - } - /// - if (conf.fingers < conf.minFingers) - return; - /// - var touches = []; - var scale = 0; - var rotation = 0; - /// Calculate centroid of gesture. - var centroidx = 0; - var centroidy = 0; - var length = 0; - for (var sid in points) { - var touch = points[sid]; - if (touch.up) - continue; - centroidx += touch.move.x; - centroidy += touch.move.y; - length++; - } - centroidx /= length; - centroidy /= length; - /// - for (var sid in points) { - var touch = points[sid]; - if (touch.up) - continue; - var start = touch.start; - if (!start.distance) { - var dx = start.x - centroidx; - var dy = start.y - centroidy; - start.distance = Math.sqrt(dx * dx + dy * dy); - start.angle = Math.atan2(dx, dy) / RAD_DEG; - } - // Calculate scale. - var dx = touch.move.x - centroidx; - var dy = touch.move.y - centroidy; - var distance = Math.sqrt(dx * dx + dy * dy); - scale += distance / start.distance; - // Calculate rotation. - var angle = Math.atan2(dx, dy) / RAD_DEG; - var rotate = (start.angle - angle + 360) % 360 - 180; - touch.DEG2 = touch.DEG1; // Previous degree. - touch.DEG1 = rotate > 0 ? rotate : -rotate; // Current degree. - if (typeof(touch.DEG2) !== "undefined") { - if (rotate > 0) { - touch.rotation += touch.DEG1 - touch.DEG2; - } else { - touch.rotation -= touch.DEG1 - touch.DEG2; - } - rotation += touch.rotation; - } - // Attach current points to self. - touches.push(touch.move); - } - /// - self.touches = touches; - self.fingers = conf.fingers; - self.scale = scale / conf.fingers; - self.rotation = rotation / conf.fingers; - self.state = "change"; - conf.listener(event, self); - }; - conf.onPointerUp = function(event) { - // Remove tracking for touch. - var fingers = conf.fingers; - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - } - // Check whether fingers has dropped below minFingers. - if (fingers === conf.minFingers && conf.fingers < conf.minFingers) { - self.fingers = conf.fingers; - self.state = "end"; - conf.listener(event, self); - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +root.gesture = function(conf) { + conf.gesture = conf.gesture || "gesture"; + conf.minFingers = conf.minFingers || conf.fingers || 2; + // Tracking the events. + conf.onPointerDown = function (event) { + var fingers = conf.fingers; + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + // Record gesture start. + if (conf.fingers === conf.minFingers && fingers !== conf.fingers) { + self.fingers = conf.minFingers; + self.scale = 1; + self.rotation = 0; + self.state = "start"; + var sids = ""; //- FIXME(mud): can generate duplicate IDs. + for (var key in conf.tracker) sids += key; + self.identifier = parseInt(sids); + conf.listener(event, self); + } + }; + /// + conf.onPointerMove = function (event, state) { + var bbox = conf.bbox; + var points = conf.tracker; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + // Update tracker coordinates. + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var pt = points[sid]; + // Check whether "pt" is used by another gesture. + if (!pt) continue; + // Find the actual coordinates. + if (conf.position === "relative") { + pt.move.x = (touch.pageX + bbox.scrollLeft - bbox.x1); + pt.move.y = (touch.pageY + bbox.scrollTop - bbox.y1); + } else { + pt.move.x = (touch.pageX - bbox.x1); + pt.move.y = (touch.pageY - bbox.y1); + } + } + /// + if (conf.fingers < conf.minFingers) return; + /// + var touches = []; + var scale = 0; + var rotation = 0; + /// Calculate centroid of gesture. + var centroidx = 0; + var centroidy = 0; + var length = 0; + for (var sid in points) { + var touch = points[sid]; + if (touch.up) continue; + centroidx += touch.move.x; + centroidy += touch.move.y; + length ++; + } + centroidx /= length; + centroidy /= length; + /// + for (var sid in points) { + var touch = points[sid]; + if (touch.up) continue; + var start = touch.start; + if (!start.distance) { + var dx = start.x - centroidx; + var dy = start.y - centroidy; + start.distance = Math.sqrt(dx * dx + dy * dy); + start.angle = Math.atan2(dx, dy) / RAD_DEG; + } + // Calculate scale. + var dx = touch.move.x - centroidx; + var dy = touch.move.y - centroidy; + var distance = Math.sqrt(dx * dx + dy * dy); + scale += distance / start.distance; + // Calculate rotation. + var angle = Math.atan2(dx, dy) / RAD_DEG; + var rotate = (start.angle - angle + 360) % 360 - 180; + touch.DEG2 = touch.DEG1; // Previous degree. + touch.DEG1 = rotate > 0 ? rotate : -rotate; // Current degree. + if (typeof(touch.DEG2) !== "undefined") { + if (rotate > 0) { + touch.rotation += touch.DEG1 - touch.DEG2; + } else { + touch.rotation -= touch.DEG1 - touch.DEG2; + } + rotation += touch.rotation; + } + // Attach current points to self. + touches.push(touch.move); + } + /// + self.touches = touches; + self.fingers = conf.fingers; + self.scale = scale / conf.fingers; + self.rotation = rotation / conf.fingers; + self.state = "change"; + conf.listener(event, self); + }; + conf.onPointerUp = function(event) { + // Remove tracking for touch. + var fingers = conf.fingers; + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + } + // Check whether fingers has dropped below minFingers. + if (fingers === conf.minFingers && conf.fingers < conf.minFingers) { + self.fingers = conf.fingers; + self.state = "end"; + conf.listener(event, self); + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.gesture = root.gesture; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.gesture = root.gesture; - return root; +return root; })(Event.proxy); /* - "Pointer" event proxy (1+ fingers). - ---------------------------------------------------- - CONFIGURE: minFingers, maxFingers. - ---------------------------------------------------- - Event.add(window, "gesture", function(event, self) { - console.log(self.rotation, self.scale, self.fingers, self.state); - }); - */ + "Pointer" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: minFingers, maxFingers. + ---------------------------------------------------- + Event.add(window, "gesture", function(event, self) { + console.log(self.rotation, self.scale, self.fingers, self.state); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.pointerdown = - root.pointermove = - root.pointerup = function(conf) { - if (conf.target.isPointerEmitter) - return; - // Tracking the events. - var isDown = true; - conf.onPointerDown = function(event) { - isDown = false; - self.gesture = "pointerdown"; - conf.listener(event, self); - }; - conf.onPointerMove = function(event) { - self.gesture = "pointermove"; - conf.listener(event, self, isDown); - }; - conf.onPointerUp = function(event) { - isDown = true; - self.gesture = "pointerup"; - conf.listener(event, self, true); - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - Event.add(conf.target, "mousemove", conf.onPointerMove); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - // Return this object. - conf.target.isPointerEmitter = true; - return self; - }; +root.pointerdown = +root.pointermove = +root.pointerup = function(conf) { + conf.gesture = conf.gesture || "pointer"; + if (conf.target.isPointerEmitter) return; + // Tracking the events. + var isDown = true; + conf.onPointerDown = function (event) { + isDown = false; + self.gesture = "pointerdown"; + conf.listener(event, self); + }; + conf.onPointerMove = function (event) { + self.gesture = "pointermove"; + conf.listener(event, self, isDown); + }; + conf.onPointerUp = function (event) { + isDown = true; + self.gesture = "pointerup"; + conf.listener(event, self, true); + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + Event.add(conf.target, "mousemove", conf.onPointerMove); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Return this object. + conf.target.isPointerEmitter = true; + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.pointerdown = root.pointerdown; - Event.Gesture._gestureHandlers.pointermove = root.pointermove; - Event.Gesture._gestureHandlers.pointerup = root.pointerup; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.pointerdown = root.pointerdown; +Event.Gesture._gestureHandlers.pointermove = root.pointermove; +Event.Gesture._gestureHandlers.pointerup = root.pointerup; - return root; +return root; })(Event.proxy); /* - "Device Motion" and "Shake" event proxy. - ---------------------------------------------------- - http://developer.android.com/reference/android/hardware/SensorEvent.html#values - ---------------------------------------------------- - Event.add(window, "shake", function(event, self) {}); - Event.add(window, "devicemotion", function(event, self) { - console.log(self.acceleration, self.accelerationIncludingGravity); - }); - */ + "Device Motion" and "Shake" event proxy. + ---------------------------------------------------- + http://developer.android.com/reference/android/hardware/SensorEvent.html#values + ---------------------------------------------------- + Event.add(window, "shake", function(event, self) {}); + Event.add(window, "devicemotion", function(event, self) { + console.log(self.acceleration, self.accelerationIncludingGravity); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.shake = function(conf) { - // Externally accessible data. - var self = { - gesture: "devicemotion", - acceleration: {}, - accelerationIncludingGravity: {}, - target: conf.target, - listener: conf.listener, - remove: function() { - window.removeEventListener('devicemotion', onDeviceMotion, false); - } - }; - // Setting up local variables. - var threshold = 4; // Gravitational threshold. - var timeout = 1000; // Timeout between shake events. - var timeframe = 200; // Time between shakes. - var shakes = 3; // Minimum shakes to trigger event. - var lastShake = (new Date).getTime(); - var gravity = {x: 0, y: 0, z: 0}; - var delta = { - x: {count: 0, value: 0}, - y: {count: 0, value: 0}, - z: {count: 0, value: 0} - }; - // Tracking the events. - var onDeviceMotion = function(e) { - var alpha = 0.8; // Low pass filter. - var o = e.accelerationIncludingGravity; - gravity.x = alpha * gravity.x + (1 - alpha) * o.x; - gravity.y = alpha * gravity.y + (1 - alpha) * o.y; - gravity.z = alpha * gravity.z + (1 - alpha) * o.z; - self.accelerationIncludingGravity = gravity; - self.acceleration.x = o.x - gravity.x; - self.acceleration.y = o.y - gravity.y; - self.acceleration.z = o.z - gravity.z; - /// - if (conf.gesture === "devicemotion") { - conf.listener(e, self); - return; - } - var data = "xyz"; - var now = (new Date).getTime(); - for (var n = 0, length = data.length; n < length; n++) { - var letter = data[n]; - var ACCELERATION = self.acceleration[letter]; - var DELTA = delta[letter]; - var abs = Math.abs(ACCELERATION); - /// Check whether another shake event was recently registered. - if (now - lastShake < timeout) - continue; - /// Check whether delta surpasses threshold. - if (abs > threshold) { - var idx = now * ACCELERATION / abs; - var span = Math.abs(idx + DELTA.value); - // Check whether last delta was registered within timeframe. - if (DELTA.value && span < timeframe) { - DELTA.value = idx; - DELTA.count++; - // Check whether delta count has enough shakes. - if (DELTA.count === shakes) { - conf.listener(e, self); - // Reset tracking. - lastShake = now; - DELTA.value = 0; - DELTA.count = 0; - } - } else { - // Track first shake. - DELTA.value = idx; - DELTA.count = 1; - } - } - } - }; - // Attach events. - if (!window.addEventListener) - return; - window.addEventListener('devicemotion', onDeviceMotion, false); - // Return this object. - return self; - }; +root.shake = function(conf) { + // Externally accessible data. + var self = { + gesture: "devicemotion", + acceleration: {}, + accelerationIncludingGravity: {}, + target: conf.target, + listener: conf.listener, + remove: function() { + window.removeEventListener('devicemotion', onDeviceMotion, false); + } + }; + // Setting up local variables. + var threshold = 4; // Gravitational threshold. + var timeout = 1000; // Timeout between shake events. + var timeframe = 200; // Time between shakes. + var shakes = 3; // Minimum shakes to trigger event. + var lastShake = (new Date()).getTime(); + var gravity = { x: 0, y: 0, z: 0 }; + var delta = { + x: { count: 0, value: 0 }, + y: { count: 0, value: 0 }, + z: { count: 0, value: 0 } + }; + // Tracking the events. + var onDeviceMotion = function(e) { + var alpha = 0.8; // Low pass filter. + var o = e.accelerationIncludingGravity; + gravity.x = alpha * gravity.x + (1 - alpha) * o.x; + gravity.y = alpha * gravity.y + (1 - alpha) * o.y; + gravity.z = alpha * gravity.z + (1 - alpha) * o.z; + self.accelerationIncludingGravity = gravity; + self.acceleration.x = o.x - gravity.x; + self.acceleration.y = o.y - gravity.y; + self.acceleration.z = o.z - gravity.z; + /// + if (conf.gesture === "devicemotion") { + conf.listener(e, self); + return; + } + var data = "xyz"; + var now = (new Date()).getTime(); + for (var n = 0, length = data.length; n < length; n ++) { + var letter = data[n]; + var ACCELERATION = self.acceleration[letter]; + var DELTA = delta[letter]; + var abs = Math.abs(ACCELERATION); + /// Check whether another shake event was recently registered. + if (now - lastShake < timeout) continue; + /// Check whether delta surpasses threshold. + if (abs > threshold) { + var idx = now * ACCELERATION / abs; + var span = Math.abs(idx + DELTA.value); + // Check whether last delta was registered within timeframe. + if (DELTA.value && span < timeframe) { + DELTA.value = idx; + DELTA.count ++; + // Check whether delta count has enough shakes. + if (DELTA.count === shakes) { + conf.listener(e, self); + // Reset tracking. + lastShake = now; + DELTA.value = 0; + DELTA.count = 0; + } + } else { + // Track first shake. + DELTA.value = idx; + DELTA.count = 1; + } + } + } + }; + // Attach events. + if (!window.addEventListener) return; + window.addEventListener('devicemotion', onDeviceMotion, false); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.shake = root.shake; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.shake = root.shake; - return root; +return root; })(Event.proxy); /* - "Swipe" event proxy (1+ fingers). - ---------------------------------------------------- - CONFIGURE: snap, threshold, maxFingers. - ---------------------------------------------------- - Event.add(window, "swipe", function(event, self) { - console.log(self.velocity, self.angle); - }); - */ + "Swipe" event proxy (1+ fingers). + ---------------------------------------------------- + CONFIGURE: snap, threshold, maxFingers. + ---------------------------------------------------- + Event.add(window, "swipe", function(event, self) { + console.log(self.velocity, self.angle); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - var RAD_DEG = Math.PI / 180; +var RAD_DEG = Math.PI / 180; - root.swipe = function(conf) { - conf.snap = conf.snap || 90; // angle snap. - conf.threshold = conf.threshold || 1; // velocity threshold. - // Tracking the events. - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - } - }; - conf.onPointerMove = function(event) { - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var sid = touch.identifier || Infinity; - var o = conf.tracker[sid]; - // Identifier defined outside of listener. - if (!o) - continue; - o.move.x = touch.pageX; - o.move.y = touch.pageY; - o.moveTime = (new Date).getTime(); - } - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - /// - var velocity1; - var velocity2 - var degree1; - var degree2; - /// Calculate centroid of gesture. - var start = {x: 0, y: 0}; - var endx = 0; - var endy = 0; - var length = 0; - /// - for (var sid in conf.tracker) { - var touch = conf.tracker[sid]; - var xdist = touch.move.x - touch.start.x; - var ydist = touch.move.y - touch.start.y; +root.swipe = function(conf) { + conf.snap = conf.snap || 90; // angle snap. + conf.threshold = conf.threshold || 1; // velocity threshold. + conf.gesture = conf.gesture || "swipe"; + // Tracking the events. + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + } + }; + conf.onPointerMove = function (event) { + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var sid = touch.identifier || Infinity; + var o = conf.tracker[sid]; + // Identifier defined outside of listener. + if (!o) continue; + o.move.x = touch.pageX; + o.move.y = touch.pageY; + o.moveTime = (new Date()).getTime(); + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + /// + var velocity1; + var velocity2 + var degree1; + var degree2; + /// Calculate centroid of gesture. + var start = { x: 0, y: 0 }; + var endx = 0; + var endy = 0; + var length = 0; + /// + for (var sid in conf.tracker) { + var touch = conf.tracker[sid]; + var xdist = touch.move.x - touch.start.x; + var ydist = touch.move.y - touch.start.y; + /// + endx += touch.move.x; + endy += touch.move.y; + start.x += touch.start.x; + start.y += touch.start.y; + length ++; + /// + var distance = Math.sqrt(xdist * xdist + ydist * ydist); + var ms = touch.moveTime - touch.startTime; + var degree2 = Math.atan2(xdist, ydist) / RAD_DEG + 180; + var velocity2 = ms ? distance / ms : 0; + if (typeof(degree1) === "undefined") { + degree1 = degree2; + velocity1 = velocity2; + } else if (Math.abs(degree2 - degree1) <= 20) { + degree1 = (degree1 + degree2) / 2; + velocity1 = (velocity1 + velocity2) / 2; + } else { + return; + } + } + /// + var fingers = conf.gestureFingers; + if (conf.minFingers <= fingers && conf.maxFingers >= fingers) { + if (velocity1 > conf.threshold) { + start.x /= length; + start.y /= length; + self.start = start; + self.x = endx / length; + self.y = endy / length; + self.angle = -((((degree1 / conf.snap + 0.5) >> 0) * conf.snap || 360) - 360); + self.velocity = velocity1; + self.fingers = fingers; + self.state = "swipe"; + conf.listener(event, self); + } + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - endx += touch.move.x; - endy += touch.move.y; - start.x += touch.start.x; - start.y += touch.start.y; - length++; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.swipe = root.swipe; - - var distance = Math.sqrt(xdist * xdist + ydist * ydist); - var ms = touch.moveTime - touch.startTime; - var degree2 = Math.atan2(xdist, ydist) / RAD_DEG + 180; - var velocity2 = ms ? distance / ms : 0; - if (typeof(degree1) === "undefined") { - degree1 = degree2; - velocity1 = velocity2; - } else if (Math.abs(degree2 - degree1) <= 20) { - degree1 = (degree1 + degree2) / 2; - velocity1 = (velocity1 + velocity2) / 2; - } else { - return; - } - } - /// - if (velocity1 > conf.threshold) { - start.x /= length; - start.y /= length; - self.start = start; - self.x = endx / length; - self.y = endy / length; - self.angle = -((((degree1 / conf.snap + 0.5) >> 0) * conf.snap || 360) - 360); - self.velocity = velocity1; - self.fingers = conf.gestureFingers; - self.state = "swipe"; - conf.listener(event, self); - } - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; - - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.swipe = root.swipe; - - return root; +return root; })(Event.proxy); /* - "Tap" and "Longpress" event proxy. - ---------------------------------------------------- - CONFIGURE: delay (longpress), timeout (tap). - ---------------------------------------------------- - Event.add(window, "tap", function(event, self) { - console.log(self.fingers); - }); - ---------------------------------------------------- - multi-finger tap // touch an target for <= 250ms. - multi-finger longpress // touch an target for >= 500ms - */ + "Tap" and "Longpress" event proxy. + ---------------------------------------------------- + CONFIGURE: delay (longpress), timeout (tap). + ---------------------------------------------------- + Event.add(window, "tap", function(event, self) { + console.log(self.fingers); + }); + ---------------------------------------------------- + multi-finger tap // touch an target for <= 250ms. + multi-finger longpress // touch an target for >= 500ms +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.tap = - root.longpress = function(conf) { - conf.delay = conf.delay || 500; - conf.timeout = conf.timeout || 250; - // Setting up local variables. - var timestamp, timeout; - // Tracking the events. - conf.onPointerDown = function(event) { - if (root.pointerStart(event, self, conf)) { - timestamp = (new Date).getTime(); - // Initialize event listeners. - Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); - Event.add(conf.doc, "mouseup", conf.onPointerUp); - // Make sure this is a "longpress" event. - if (conf.gesture !== "longpress") - return; - timeout = setTimeout(function() { - if (event.cancelBubble && ++event.bubble > 1) - return; - // Make sure no fingers have been changed. - var fingers = 0; - for (var key in conf.tracker) { - if (conf.tracker[key].end === true) - return; - if (conf.cancel) - return; - fingers++; - } - // Send callback. - self.state = "start"; - self.fingers = fingers; - conf.listener(event, self); - }, conf.delay); - } - }; - conf.onPointerMove = function(event) { - var bbox = conf.bbox; - var touches = event.changedTouches || root.getCoords(event); - var length = touches.length; - for (var i = 0; i < length; i++) { - var touch = touches[i]; - var identifier = touch.identifier || Infinity; - var pt = conf.tracker[identifier]; - if (!pt) - continue; - if (conf.position === "relative") { - var x = (touch.pageX + bbox.scrollLeft - bbox.x1) * bbox.scaleX; - var y = (touch.pageY + bbox.scrollTop - bbox.y1) * bbox.scaleY; - } else { - var x = (touch.pageX - bbox.x1); - var y = (touch.pageY - bbox.y1); - } - if (!(x > 0 && x < bbox.width && // Within target coordinates.. - y > 0 && y < bbox.height && - Math.abs(x - pt.start.x) <= 25 && // Within drift deviance. - Math.abs(y - pt.start.y) <= 25)) { - // Cancel out this listener. - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - conf.cancel = true; - return; - } - } - }; - conf.onPointerUp = function(event) { - if (root.pointerEnd(event, self, conf)) { - clearTimeout(timeout); - Event.remove(conf.doc, "mousemove", conf.onPointerMove); - Event.remove(conf.doc, "mouseup", conf.onPointerUp); - if (event.cancelBubble && ++event.bubble > 1) - return; - // Callback release on longpress. - if (conf.gesture === "longpress") { - if (self.state === "start") { - self.state = "end"; - conf.listener(event, self); - } - return; - } - // Cancel event due to movement. - if (conf.cancel) - return; - // Ensure delay is within margins. - if ((new Date).getTime() - timestamp > conf.timeout) - return; - // Send callback. - self.state = "tap"; - self.fingers = conf.gestureFingers; - conf.listener(event, self); - } - }; - // Generate maintenance commands, and other configurations. - var self = root.pointerSetup(conf); - // Attach events. - Event.add(conf.target, "mousedown", conf.onPointerDown); - // Return this object. - return self; - }; +root.longpress = function(conf) { + conf.gesture = "longpress"; + return root.tap(conf); +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.tap = root.tap; - Event.Gesture._gestureHandlers.longpress = root.longpress; +root.tap = function(conf) { + conf.delay = conf.delay || 500; + conf.timeout = conf.timeout || 250; + conf.driftDeviance = conf.driftDeviance || 10; + conf.gesture = conf.gesture || "tap"; + // Setting up local variables. + var timestamp, timeout; + // Tracking the events. + conf.onPointerDown = function (event) { + if (root.pointerStart(event, self, conf)) { + timestamp = (new Date()).getTime(); + // Initialize event listeners. + Event.add(conf.doc, "mousemove", conf.onPointerMove).listener(event); + Event.add(conf.doc, "mouseup", conf.onPointerUp); + // Make sure this is a "longpress" event. + if (conf.gesture !== "longpress") return; + timeout = setTimeout(function() { + if (event.cancelBubble && ++event.bubble > 1) return; + // Make sure no fingers have been changed. + var fingers = 0; + for (var key in conf.tracker) { + var point = conf.tracker[key]; + if (point.end === true) return; + if (conf.cancel) return; + fingers ++; + } + // Send callback. + if (conf.minFingers <= fingers && conf.maxFingers >= fingers) { + self.state = "start"; + self.fingers = fingers; + self.x = point.start.x; + self.y = point.start.y; + conf.listener(event, self); + } + }, conf.delay); + } + }; + conf.onPointerMove = function (event) { + var bbox = conf.bbox; + var touches = event.changedTouches || root.getCoords(event); + var length = touches.length; + for (var i = 0; i < length; i ++) { + var touch = touches[i]; + var identifier = touch.identifier || Infinity; + var pt = conf.tracker[identifier]; + if (!pt) continue; + if (conf.position === "relative") { + var x = (touch.pageX + bbox.scrollLeft - bbox.x1); + var y = (touch.pageY + bbox.scrollTop - bbox.y1); + } else { + var x = (touch.pageX - bbox.x1); + var y = (touch.pageY - bbox.y1); + } + /// + var dx = x - pt.start.x; + var dy = y - pt.start.y; + var distance = Math.sqrt(dx * dx + dy * dy); + if (!(x > 0 && x < bbox.width && // Within target coordinates.. + y > 0 && y < bbox.height && + distance <= conf.driftDeviance)) { // Within drift deviance. + // Cancel out this listener. + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + conf.cancel = true; + return; + } + } + }; + conf.onPointerUp = function(event) { + if (root.pointerEnd(event, self, conf)) { + clearTimeout(timeout); + Event.remove(conf.doc, "mousemove", conf.onPointerMove); + Event.remove(conf.doc, "mouseup", conf.onPointerUp); + if (event.cancelBubble && ++event.bubble > 1) return; + // Callback release on longpress. + if (conf.gesture === "longpress") { + if (self.state === "start") { + self.state = "end"; + conf.listener(event, self); + } + return; + } + // Cancel event due to movement. + if (conf.cancel) return; + // Ensure delay is within margins. + if ((new Date()).getTime() - timestamp > conf.timeout) return; + // Send callback. + var fingers = conf.gestureFingers; + if (conf.minFingers <= fingers && conf.maxFingers >= fingers) { + self.state = "tap"; + self.fingers = conf.gestureFingers; + conf.listener(event, self); + } + } + }; + // Generate maintenance commands, and other configurations. + var self = root.pointerSetup(conf); + // Attach events. + Event.add(conf.target, "mousedown", conf.onPointerDown); + // Return this object. + return self; +}; - return root; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.tap = root.tap; +Event.Gesture._gestureHandlers.longpress = root.longpress; + +return root; })(Event.proxy); /* - "Mouse Wheel" event proxy. - ---------------------------------------------------- - Event.add(window, "wheel", function(event, self) { - console.log(self.state, self.wheelDelta); - }); - */ + "Mouse Wheel" event proxy. + ---------------------------------------------------- + Event.add(window, "wheel", function(event, self) { + console.log(self.state, self.wheelDelta); + }); +*/ -if (typeof(Event) === "undefined") - var Event = {}; -if (typeof(Event.proxy) === "undefined") - Event.proxy = {}; +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; -Event.proxy = (function(root) { - "use strict"; +Event.proxy = (function(root) { "use strict"; - root.wheel = function(conf) { - // Configure event listener. - var interval; - var timeout = conf.timeout || 150; - var count = 0; - // Externally accessible data. - var self = { - gesture: "wheel", - state: "start", - wheelDelta: 0, - target: conf.target, - listener: conf.listener, - remove: function() { - conf.target[remove](type, onMouseWheel, false); - } - }; - // Tracking the events. - var onMouseWheel = function(event) { - event = event || window.event; - self.state = count++ ? "change" : "start"; - self.wheelDelta = event.detail ? event.detail * -20 : event.wheelDelta; - conf.listener(event, self); - clearTimeout(interval); - interval = setTimeout(function() { - count = 0; - self.state = "end"; - self.wheelDelta = 0; - conf.listener(event, self); - }, timeout); - }; - // Attach events. - var add = document.addEventListener ? "addEventListener" : "attachEvent"; - var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; - var type = Event.supports("mousewheel") ? "mousewheel" : "DOMMouseScroll"; - conf.target[add](type, onMouseWheel, false); - // Return this object. - return self; - }; +root.wheel = function(conf) { + // Configure event listener. + var interval; + var timeout = conf.timeout || 150; + var count = 0; + // Externally accessible data. + var self = { + gesture: "wheel", + state: "start", + wheelDelta: 0, + target: conf.target, + listener: conf.listener, + preventElasticBounce: function() { + var target = this.target; + var scrollTop = target.scrollTop; + var top = scrollTop + target.offsetHeight; + var height = target.scrollHeight; + if (top === height && this.wheelDelta <= 0) Event.cancel(event); + else if (scrollTop === 0 && this.wheelDelta >= 0) Event.cancel(event); + Event.stop(event); + }, + add: function() { + conf.target[add](type, onMouseWheel, false); + }, + remove: function() { + conf.target[remove](type, onMouseWheel, false); + } + }; + // Tracking the events. + var onMouseWheel = function(event) { + event = event || window.event; + self.state = count++ ? "change" : "start"; + self.wheelDelta = event.detail ? event.detail * -20 : event.wheelDelta; + conf.listener(event, self); + clearTimeout(interval); + interval = setTimeout(function() { + count = 0; + self.state = "end"; + self.wheelDelta = 0; + conf.listener(event, self); + }, timeout); + }; + // Attach events. + var add = document.addEventListener ? "addEventListener" : "attachEvent"; + var remove = document.removeEventListener ? "removeEventListener" : "detachEvent"; + var type = Event.getEventSupport("mousewheel") ? "mousewheel" : "DOMMouseScroll"; + conf.target[add](type, onMouseWheel, false); + // Return this object. + return self; +}; - Event.Gesture = Event.Gesture || {}; - Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; - Event.Gesture._gestureHandlers.wheel = root.wheel; +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.wheel = root.wheel; - return root; +return root; + +})(Event.proxy); +/* + "Orientation Change" + ---------------------------------------------------- + https://developer.apple.com/library/safari/documentation/SafariDOMAdditions/Reference/DeviceOrientationEventClassRef/DeviceOrientationEvent/DeviceOrientationEvent.html#//apple_ref/doc/uid/TP40010526 + ---------------------------------------------------- + Event.add(window, "deviceorientation", function(event, self) {}); +*/ + +if (typeof(Event) === "undefined") var Event = {}; +if (typeof(Event.proxy) === "undefined") Event.proxy = {}; + +Event.proxy = (function(root) { "use strict"; + +root.orientation = function(conf) { + // Externally accessible data. + var self = { + gesture: "orientationchange", + previous: null, /* Report the previous orientation */ + current: window.orientation, + target: conf.target, + listener: conf.listener, + remove: function() { + window.removeEventListener('orientationchange', onOrientationChange, false); + } + }; + + // Tracking the events. + var onOrientationChange = function(e) { + + self.previous = self.current; + self.current = window.orientation; + if(self.previous !== null && self.previous != self.current) { + conf.listener(e, self); + return; + } + + + }; + // Attach events. + if (window.DeviceOrientationEvent) { + window.addEventListener("orientationchange", onOrientationChange, false); + } + // Return this object. + return self; +}; + +Event.Gesture = Event.Gesture || {}; +Event.Gesture._gestureHandlers = Event.Gesture._gestureHandlers || {}; +Event.Gesture._gestureHandlers.orientation = root.orientation; + +return root; })(Event.proxy); diff --git a/lib/google_closure_compiler.jar b/lib/google_closure_compiler.jar index 484e8741..fc290866 100644 Binary files a/lib/google_closure_compiler.jar and b/lib/google_closure_compiler.jar differ diff --git a/package.json b/package.json index 885ac7a4..53568296 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fabric", "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.", - "version": "1.3.7", + "version": "1.3.12", "author": "Juriy Zaytsev ", "keywords": ["canvas", "graphic", "graphics", "SVG", "node-canvas", "parser", "HTML5", "object model"], "repository": "git://github.com/kangax/fabric.js", @@ -20,8 +20,8 @@ }, "devDependencies": { "qunit": "0.5.x", - "jshint": "2.1.x", - "uglify-js": "2.3.x", + "jshint": "2.3.x", + "uglify-js": "2.4.x", "execSync": "0.0.x", "plato": "0.6.x" }, diff --git a/src/amd/requirejs.js b/src/amd/requirejs.js index 2c3599df..26a2696d 100644 --- a/src/amd/requirejs.js +++ b/src/amd/requirejs.js @@ -7,5 +7,5 @@ var exports = exports || {}; exports.fabric = fabric; if (typeof define === "function" && define.amd) { - define("fabric", [], function() { return fabric }); + define([], function() { return fabric }); } diff --git a/src/brushes/circle_brush.class.js b/src/brushes/circle_brush.class.js index 39794345..2302bb55 100644 --- a/src/brushes/circle_brush.class.js +++ b/src/brushes/circle_brush.class.js @@ -74,6 +74,8 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric radius: this.points[i].radius, left: point.x, top: point.y, + originX: 'center', + originY: 'center', fill: this.points[i].fill }); @@ -81,7 +83,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } - var group = new fabric.Group(circles); + var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); group.canvas = this.canvas; this.canvas.add(group); diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index 2cf8d156..d6a2c947 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -111,7 +111,7 @@ var p1 = this._points[0]; var p2 = this._points[1]; - + //if we only have 2 points in the path and they are the same //it means that the user only clicked the canvas without moving the mouse //then we should be drawing a dot. A path isn't drawn between two identical dots @@ -260,7 +260,12 @@ this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false); var path = this.createPath(pathData); - path.set({ left: originLeft, top: originTop }); + path.set({ + left: originLeft, + top: originTop, + originX: 'center', + originY: 'center' + }); this.canvas.add(path); path.setCoords(); diff --git a/src/brushes/spray_brush.class.js b/src/brushes/spray_brush.class.js index 80b0466e..ad75c16d 100644 --- a/src/brushes/spray_brush.class.js +++ b/src/brushes/spray_brush.class.js @@ -97,6 +97,8 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric height: sprayChunk[j].width, left: sprayChunk[j].x + 1, top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', fill: this.color }); @@ -109,7 +111,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects); + var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); group.canvas = this.canvas; this.canvas.add(group); diff --git a/src/brushes/| b/src/brushes/| new file mode 100644 index 00000000..2302bb55 --- /dev/null +++ b/src/brushes/| @@ -0,0 +1,119 @@ +/** + * CircleBrush class + * @class fabric.CircleBrush + */ +fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { + + /** + * Width of a brush + * @type Number + * @default + */ + width: 10, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.CircleBrush} Instance of a circle brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.points = [ ]; + }, + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + drawDot: function(pointer) { + var point = this.addPoint(pointer); + var ctx = this.canvas.contextTop; + + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + + ctx.restore(); + }, + + /** + * Invoked on mouse down + */ + onMouseDown: function(pointer) { + this.points.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + this.drawDot(pointer); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + this.drawDot(pointer); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + var circles = [ ]; + + for (var i = 0, len = this.points.length; i < len; i++) { + var point = this.points[i]; + var circle = new fabric.Circle({ + radius: this.points[i].radius, + left: point.x, + top: point.y, + originX: 'center', + originY: 'center', + fill: this.points[i].fill + }); + + this.shadow && circle.setShadow(this.shadow); + + circles.push(circle); + } + var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); + group.canvas = this.canvas; + + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.renderAll(); + }, + + /** + * @param {Object} pointer + * @return {fabric.Point} Just added pointer point + */ + addPoint: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + + var circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2; + + var circleColor = new fabric.Color(this.color) + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); + + pointerPoint.radius = circleRadius; + pointerPoint.fill = circleColor; + + this.points.push(pointerPoint); + + return pointerPoint; + } +}); diff --git a/src/canvas.class.js b/src/canvas.class.js index baa12014..36cad3f0 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -5,8 +5,6 @@ radiansToDegrees = fabric.util.radiansToDegrees, atan2 = Math.atan2, abs = Math.abs, - min = Math.min, - max = Math.max, STROKE_OFFSET = 0.5; @@ -15,6 +13,7 @@ * @class fabric.Canvas * @extends fabric.StaticCanvas * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition * * @fires object:modified * @fires object:rotating @@ -188,7 +187,7 @@ this._groupSelector = null; this._initWrapperElement(); this._createUpperCanvas(); - this._initEvents(); + this._initEventListeners(); this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); @@ -289,47 +288,20 @@ * @return {Boolean} */ isTargetTransparent: function (target, x, y) { - var cacheContext = this.contextCache; - var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; target.hasBorders = target.transparentCorners = false; - this._draw(cacheContext, target); + this._draw(this.contextCache, target); target.hasBorders = hasBorders; target.transparentCorners = transparentCorners; - // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 - if (this.targetFindTolerance > 0) { - if (x > this.targetFindTolerance) { - x -= this.targetFindTolerance; - } - else { - x = 0; - } - if (y > this.targetFindTolerance) { - y -= this.targetFindTolerance; - } - else { - y = 0; - } - } + var isTransparent = fabric.util.isTransparent( + this.contextCache, x, y, this.targetFindTolerance); - var isTransparent = true; - var imageData = cacheContext.getImageData( - x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (var i = 3, l = imageData.data.length; i < l; i += 4) { - var temp = imageData.data[i]; - isTransparent = temp <= 0; - if (isTransparent === false) break; //Stop if colour found - } - - imageData = null; - this.clearContext(cacheContext); + this.clearContext(this.contextCache); return isTransparent; }, @@ -340,17 +312,24 @@ * @param {fabric.Object} target */ _shouldClearSelection: function (e, target) { - var activeGroup = this.getActiveGroup(); + var activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(); return ( - !target || ( - target && - activeGroup && - !activeGroup.contains(target) && - activeGroup !== target && - !e.shiftKey) || ( - target && - !target.evented) + !target + || + (target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e.shiftKey) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) ); }, @@ -377,20 +356,35 @@ /** * @private - * @param {Event} e Event object - * @param {fabric.Object} target */ - _setupCurrentTransform: function (e, target) { - if (!target) return; + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; - var action = 'drag', - corner, - pointer = fabric.util.transformPoint( - getPointer(e, this.upperCanvasEl), - fabric.util.invertTransform(this.viewportTransform) - ); + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } - corner = target._findTargetCorner(e, this._offset); + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + + return origin; + }, + + /** + * @private + */ + _getActionFromCorner: function(target, corner) { + var action = 'drag'; if (corner) { action = (corner === 'ml' || corner === 'mr') ? 'scaleX' @@ -400,23 +394,24 @@ ? 'rotate' : 'scale'; } + return action; + }, - var originX = target.originX, - originY = target.originY; + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target) { + if (!target) return; - if (corner === 'ml' || corner === 'tl' || corner === 'bl') { - originX = "right"; - } - else if (corner === 'mr' || corner === 'tr' || corner === 'br') { - originX = "left"; - } - - if (corner === 'tl' || corner === 'mt' || corner === 'tr') { - originY = "bottom"; - } - else if (corner === 'bl' || corner === 'mb' || corner === 'br') { - originY = "top"; - } + var corner = target._findTargetCorner(e, this._offset), + pointer = fabric.util.transformPoint( + getPointer(e, this.upperCanvasEl), + fabric.util.invertTransform(this.viewportTransform) + ), + action = this._getActionFromCorner(target, corner), + origin = this._getOriginFromCorner(target, corner); this._currentTransform = { target: target, @@ -425,8 +420,8 @@ scaleY: target.scaleY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, - originX: originX, - originY: originY, + originX: origin.x, + originY: origin.y, ex: pointer.x, ey: pointer.y, left: target.left, @@ -442,84 +437,13 @@ top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, - originX: originX, - originY: originY + originX: origin.x, + originY: origin.y }; this._resetCurrentTransform(e); }, - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - * @return {Boolean} - */ - _shouldHandleGroupLogic: function(e, target) { - var activeObject = this.getActiveObject(); - return e.shiftKey && - (this.getActiveGroup() || (activeObject && activeObject !== target)) - && this.selection; - }, - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - */ - _handleGroupLogic: function (e, target) { - if (target === this.getActiveGroup()) { - // if it's a group, find target again, this time skipping group - target = this.findTarget(e, true); - // if even object is not found, bail out - if (!target || target.isType('group')) { - return; - } - } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - if (activeGroup.contains(target)) { - activeGroup.removeWithUpdate(target); - this._resetObjectTransform(activeGroup); - target.set('active', false); - if (activeGroup.size() === 1) { - // remove group alltogether if after removal it only contains 1 object - this.discardActiveGroup(); - } - } - else { - activeGroup.addWithUpdate(target); - this._resetObjectTransform(activeGroup); - } - this.fire('selection:created', { target: activeGroup, e: e }); - activeGroup.set('active', true); - } - else { - // group does not exist - if (this._activeObject) { - // only if there's an active object - if (target !== this._activeObject) { - // and that object is not the actual target - var objects = this.getObjects(); - var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); - var group = new fabric.Group( - isActiveLower ? [ target, this._activeObject ] : [ this._activeObject, target ]); - - this.setActiveGroup(group); - this._activeObject = null; - activeGroup = this.getActiveGroup(); - this.fire('selection:created', { target: activeGroup, e: e }); - } - } - // activate target object in any case - target.set('active', true); - } - - if (activeGroup) { - activeGroup.saveCoords(); - } - }, - /** * Translates object by "setting" its left/top * @private @@ -548,9 +472,8 @@ _scaleObject: function (x, y, by) { var t = this._currentTransform, offset = this._offset, - target = t.target; - - var lockScalingX = target.get('lockScalingX'), + target = t.target, + lockScalingX = target.get('lockScalingX'), lockScalingY = target.get('lockScalingY'); if (lockScalingX && lockScalingY) return; @@ -559,6 +482,94 @@ var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); var localMouse = target.toLocalPoint(new fabric.Point(x - offset.left, y - offset.top), t.originX, t.originY); + this._setLocalMouse(localMouse, t); + + // Actually scale the object + this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by); + + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + }, + + /** + * @private + */ + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by) { + var target = transform.target; + + transform.newScaleX = target.scaleX; + transform.newScaleY = target.scaleY; + + if (by === 'equally' && !lockScalingX && !lockScalingY) { + this._scaleObjectEqually(localMouse, target, transform); + } + else if (!by) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + + lockScalingX || target.set('scaleX', transform.newScaleX); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + else if (by === 'x' && !target.get('lockUniScaling')) { + transform.newScaleX = localMouse.x / (target.width + target.strokeWidth); + lockScalingX || target.set('scaleX', transform.newScaleX); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + transform.newScaleY = localMouse.y / (target.height + target.strokeWidth); + lockScalingY || target.set('scaleY', transform.newScaleY); + } + + this._flipObject(transform); + }, + + /** + * @private + */ + _scaleObjectEqually: function(localMouse, target, transform) { + + var dist = localMouse.y + localMouse.x; + + var lastDist = (target.height + (target.strokeWidth)) * transform.original.scaleY + + (target.width + (target.strokeWidth)) * transform.original.scaleX; + + // We use transform.scaleX/Y instead of target.scaleX/Y + // because the object may have a min scale and we'll loose the proportions + transform.newScaleX = transform.original.scaleX * dist / lastDist; + transform.newScaleY = transform.original.scaleY * dist / lastDist; + + target.set('scaleX', transform.newScaleX); + target.set('scaleY', transform.newScaleY); + }, + + /** + * @private + */ + _flipObject: function(transform) { + if (transform.newScaleX < 0) { + if (transform.originX === 'left') { + transform.originX = 'right'; + } + else if (transform.originX === 'right') { + transform.originX = 'left'; + } + } + + if (transform.newScaleY < 0) { + if (transform.originY === 'top') { + transform.originY = 'bottom'; + } + else if (transform.originY === 'bottom') { + transform.originY = 'top'; + } + } + }, + + /** + * @private + */ + _setLocalMouse: function(localMouse, t) { + var target = t.target; + if (t.originX === 'right') { localMouse.x *= -1; } @@ -583,74 +594,28 @@ // adjust the mouse coordinates when dealing with padding if (abs(localMouse.x) > target.padding) { - if (localMouse.x < 0 ) { + if (localMouse.x < 0) { localMouse.x += target.padding; - } else { + } + else { localMouse.x -= target.padding; } - } else { // mouse is within the padding, set to 0 + } + else { // mouse is within the padding, set to 0 localMouse.x = 0; } if (abs(localMouse.y) > target.padding) { - if (localMouse.y < 0 ) { + if (localMouse.y < 0) { localMouse.y += target.padding; - } else { + } + else { localMouse.y -= target.padding; } - } else { + } + else { localMouse.y = 0; } - - // Actually scale the object - var newScaleX = target.scaleX, newScaleY = target.scaleY; - if (by === 'equally' && !lockScalingX && !lockScalingY) { - var dist = localMouse.y + localMouse.x; - var lastDist = (target.height + (target.strokeWidth)) * t.original.scaleY + - (target.width + (target.strokeWidth)) * t.original.scaleX; - - // We use t.scaleX/Y instead of target.scaleX/Y because the object may have a min scale and we'll loose the proportions - newScaleX = t.original.scaleX * dist/lastDist; - newScaleY = t.original.scaleY * dist/lastDist; - - target.set('scaleX', newScaleX); - target.set('scaleY', newScaleY); - } - else if (!by) { - newScaleX = localMouse.x/(target.width+target.strokeWidth); - newScaleY = localMouse.y/(target.height+target.strokeWidth); - - lockScalingX || target.set('scaleX', newScaleX); - lockScalingY || target.set('scaleY', newScaleY); - } - else if (by === 'x' && !target.get('lockUniScaling')) { - newScaleX = localMouse.x/(target.width + target.strokeWidth); - lockScalingX || target.set('scaleX', newScaleX); - } - else if (by === 'y' && !target.get('lockUniScaling')) { - newScaleY = localMouse.y/(target.height + target.strokeWidth); - lockScalingY || target.set('scaleY', newScaleY); - } - - // Check if we flipped - if (newScaleX < 0) - { - if (t.originX === 'left') - t.originX = 'right'; - else if (t.originX === 'right') - t.originX = 'left'; - } - - if (newScaleY < 0) - { - if (t.originY === 'top') - t.originY = 'bottom'; - else if (t.originY === 'bottom') - t.originY = 'top'; - } - - // Make sure the constraints apply - target.setPositionByOrigin(constraintPosition, t.originX, t.originY); }, /** @@ -746,48 +711,13 @@ /** * @private */ - _findSelectedObjects: function (e) { - var group = [ ], - x1 = this._groupSelector.ex, - y1 = this._groupSelector.ey, - x2 = x1 + this._groupSelector.left, - y2 = y1 + this._groupSelector.top, - currentObject, - selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), - selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), - isClick = x1 === x2 && y1 === y2; - - for (var i = this._objects.length; i--; ) { - currentObject = this._objects[i]; - - if (!currentObject) continue; - - if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || - currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || - currentObject.containsPoint(selectionX1Y1) || - currentObject.containsPoint(selectionX2Y2)) { - - if (this.selection && currentObject.selectable) { - currentObject.set('active', true); - group.push(currentObject); - - // only add one object if it's a click - if (isClick) break; - } - } - } - - // do not create group for 1 element only - if (group.length === 1) { - this.setActiveObject(group[0], e); - } - else if (group.length > 1) { - group = new fabric.Group(group.reverse()); - this.setActiveGroup(group); - group.saveCoords(); - this.fire('selection:created', { target: group }); - this.renderAll(); - } + _isLastRenderedObject: function(e) { + return ( + this.controlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay && + this.lastRenderedObjectWithControlsAboveOverlay.visible && + this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && + this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)); }, /** @@ -798,31 +728,30 @@ findTarget: function (e, skipGroup) { if (this.skipTargetFind) return; - var target, - pointer = this.getPointer(e, true); - - if (this.controlsAboveOverlay && - this.lastRenderedObjectWithControlsAboveOverlay && - this.lastRenderedObjectWithControlsAboveOverlay.visible && - this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && - this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)) { - target = this.lastRenderedObjectWithControlsAboveOverlay; - return target; + if (this._isLastRenderedObject(e)) { + return this.lastRenderedObjectWithControlsAboveOverlay; } // first check current group (if one exists) var activeGroup = this.getActiveGroup(); if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { - target = activeGroup; - return target; + return activeGroup; } - // then check all of the objects on canvas + return this._searchPossibleTargets(e); + }, + + /** + * @private + */ + _searchPossibleTargets: function(e) { + // Cache all targets where their bounding box contains point. - var possibleTargets = []; + var possibleTargets = [], + target, + pointer = this.getPointer(e, true); for (var i = this._objects.length; i--; ) { - if (this._objects[i] && this._objects[i].visible && this._objects[i].evented && @@ -838,6 +767,7 @@ } } } + for (var j = 0, len = possibleTargets.length; j < len; j++) { pointer = this.getPointer(e, true); var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); @@ -965,6 +895,18 @@ return this.upperCanvasEl; }, + /** + * @private + * @param {Object} object + */ + _setActiveObject: function(object) { + if (this._activeObject) { + this._activeObject.set('active', false); + } + this._activeObject = object; + object.set('active', true); + }, + /** * Sets given object as the only active object on canvas * @param {fabric.Object} object Object to set as an active one @@ -973,14 +915,8 @@ * @chainable */ setActiveObject: function (object, e) { - if (this._activeObject) { - this._activeObject.set('active', false); - } - this._activeObject = object; - object.set('active', true); - + this._setActiveObject(object); this.renderAll(); - this.fire('object:selected', { target: object, e: e }); object.fire('selected', { e: e }); return this; @@ -995,32 +931,53 @@ }, /** - * Discards currently active object - * @return {fabric.Canvas} thisArg - * @chainable + * @private */ - discardActiveObject: function () { + _discardActiveObject: function() { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = null; + }, + + /** + * Discards currently active object + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + this._discardActiveObject(); + this.renderAll(); + this.fire('selection:cleared', { e: e }); return this; }, + /** + * @private + * @param {fabric.Group} group + */ + _setActiveGroup: function(group) { + this._activeGroup = group; + if (group) { + group.canvas = this; + group._calcBounds(); + group._updateObjectsCoords(); + group.setCoords(); + group.set('active', true); + } + }, + /** * Sets active group to a speicified one * @param {fabric.Group} group Group to set as a current one * @return {fabric.Canvas} thisArg * @chainable */ - setActiveGroup: function (group) { - this._activeGroup = group; + setActiveGroup: function (group, e) { + this._setActiveGroup(group); if (group) { - group.canvas = this; - group._calcBounds(); - group._updateObjectsCoords(); - group.setCoords(); - group.set('active', true); + this.fire('object:selected', { target: group, e: e }); + group.fire('selected', { e: e }); } return this; }, @@ -1034,15 +991,24 @@ }, /** - * Removes currently active group - * @return {fabric.Canvas} thisArg + * @private */ - discardActiveGroup: function () { + _discardActiveGroup: function() { var g = this.getActiveGroup(); if (g) { g.destroy(); } - return this.setActiveGroup(null); + this.setActiveGroup(null); + }, + + /** + * Discards currently active group + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function (e) { + this._discardActiveGroup(); + this.fire('selection:cleared', { e: e }); + return this; }, /** @@ -1056,8 +1022,8 @@ for ( ; i < len; i++) { allObjects[i].set('active', false); } - this.discardActiveGroup(); - this.discardActiveObject(); + this._discardActiveGroup(); + this._discardActiveObject(); return this; }, @@ -1065,14 +1031,14 @@ * Deactivates all objects and dispatches appropriate events * @return {fabric.Canvas} thisArg */ - deactivateAllWithDispatch: function () { + deactivateAllWithDispatch: function (e) { var activeObject = this.getActiveGroup() || this.getActiveObject(); if (activeObject) { - this.fire('before:selection:cleared', { target: activeObject }); + this.fire('before:selection:cleared', { target: activeObject, e: e }); } this.deactivateAll(); if (activeObject) { - this.fire('selection:cleared'); + this.fire('selection:cleared', { e: e }); } return this; }, @@ -1084,23 +1050,39 @@ drawControls: function(ctx) { var activeGroup = this.getActiveGroup(); if (activeGroup) { - ctx.save(); - fabric.Group.prototype.transform.call(activeGroup, ctx); - activeGroup.drawBorders(ctx).drawControls(ctx); - ctx.restore(); + this._drawGroupControls(ctx, activeGroup); } else { - for (var i = 0, len = this._objects.length; i < len; ++i) { - if (!this._objects[i] || !this._objects[i].active) continue; - - ctx.save(); - fabric.Object.prototype.transform.call(this._objects[i], ctx); - this._objects[i].drawBorders(ctx).drawControls(ctx); - ctx.restore(); - - this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; - } + this._drawObjectsControls(ctx); } + }, + + /** + * @private + */ + _drawGroupControls: function(ctx, activeGroup) { + this._drawControls(ctx, activeGroup, 'Group'); + }, + + /** + * @private + */ + _drawObjectsControls: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i] || !this._objects[i].active) continue; + this._drawControls(ctx, this._objects[i], 'Object'); + this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; + } + }, + + /** + * @private + */ + _drawControls: function(ctx, object, klass) { + ctx.save(); + fabric[klass].prototype.transform.call(object, ctx); + object.drawBorders(ctx).drawControls(ctx); + ctx.restore(); } }); diff --git a/src/elements_parser.js b/src/elements_parser.js new file mode 100644 index 00000000..caf548ea --- /dev/null +++ b/src/elements_parser.js @@ -0,0 +1,67 @@ +fabric.ElementsParser = { + + parse: function(elements, callback, options, reviver) { + + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + + this.instances = new Array(elements.length); + this.numElements = elements.length; + + this.createObjects(); + }, + + createObjects: function() { + for (var i = 0, len = this.elements.length; i < len; i++) { + this.createObject(this.elements[i], i); + } + }, + + createObject: function(el, index) { + var klass = fabric[fabric.util.string.capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + this._createObject(klass, el, index); + } + catch(err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }, + + _createObject: function(klass, el, index) { + if (klass.async) { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + else { + var obj = klass.fromElement(el, this.options); + this.reviver && this.reviver(el, obj); + this.instances.splice(index, 0, obj); + this.checkIfDone(); + } + }, + + createCallback: function(index, el) { + var _this = this; + return function(obj) { + _this.reviver && _this.reviver(el, obj); + _this.instances.splice(index, 0, obj); + _this.checkIfDone(); + }; + }, + + checkIfDone: function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + return el != null; + }); + fabric.resolveGradients(this.instances); + this.callback(this.instances); + } + } +}; diff --git a/src/filters/brightness_filter.class.js b/src/filters/brightness_filter.class.js index 2e37d4c8..4fe03d5d 100644 --- a/src/filters/brightness_filter.class.js +++ b/src/filters/brightness_filter.class.js @@ -10,6 +10,14 @@ * @class fabric.Image.filters.Brightness * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Brightness = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { diff --git a/src/filters/convolute_filter.class.js b/src/filters/convolute_filter.class.js index e354901a..a63e7175 100644 --- a/src/filters/convolute_filter.class.js +++ b/src/filters/convolute_filter.class.js @@ -10,6 +10,41 @@ * @class fabric.Image.filters.Convolute * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Convolute = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { @@ -51,15 +86,16 @@ * @param {Object} canvasEl Canvas element to apply filter to */ applyTo: function(canvasEl) { - var weights = this.matrix; - var context = canvasEl.getContext('2d'); - var pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height); - var side = Math.round(Math.sqrt(weights.length)); - var halfSide = Math.floor(side/2); - var src = pixels.data; - var sw = pixels.width; - var sh = pixels.height; + var weights = this.matrix, + context = canvasEl.getContext('2d'), + pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side/2), + src = pixels.data, + sw = pixels.width, + sh = pixels.height; // pad output by the convolution matrix var w = sw; @@ -70,6 +106,7 @@ // go through the destination image pixels var alphaFac = this.opaque ? 1 : 0; + for (var y=0; y= 0 && scy < sh && scx >= 0 && scx < sw) { - var srcOff = (scy*sw+scx)*4; - var wt = weights[cy*side+cx]; - r += src[srcOff] * wt; - g += src[srcOff+1] * wt; - b += src[srcOff+2] * wt; - a += src[srcOff+3] * wt; - } + + /* jshint maxdepth:5 */ + if (scy < 0 || scy > sh || scx < 0 || scx > sw) continue; + + var srcOff = (scy*sw+scx)*4; + var wt = weights[cy*side+cx]; + + r += src[srcOff] * wt; + g += src[srcOff+1] * wt; + b += src[srcOff+2] * wt; + a += src[srcOff+3] * wt; } } dst[dstOff] = r; diff --git a/src/filters/gradienttransparency_filter.class.js b/src/filters/gradienttransparency_filter.class.js index 3a2df791..9f7ea01f 100644 --- a/src/filters/gradienttransparency_filter.class.js +++ b/src/filters/gradienttransparency_filter.class.js @@ -10,6 +10,14 @@ * @class fabric.Image.filters.GradientTransparency * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.GradientTransparency#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.GradientTransparency({ + * threshold: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.GradientTransparency = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { @@ -22,7 +30,7 @@ /** * Constructor - * @memberOf fabric.Image.filters.GradientTransparency + * @memberOf fabric.Image.filters.GradientTransparency.prototype * @param {Object} [options] Options object * @param {Number} [options.threshold=100] Threshold value */ diff --git a/src/filters/grayscale_filter.class.js b/src/filters/grayscale_filter.class.js index 7cc41552..34e61e76 100644 --- a/src/filters/grayscale_filter.class.js +++ b/src/filters/grayscale_filter.class.js @@ -9,6 +9,11 @@ * @class fabric.Image.filters.Grayscale * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Grayscale = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { diff --git a/src/filters/invert_filter.class.js b/src/filters/invert_filter.class.js index 2524a0a4..1478863b 100644 --- a/src/filters/invert_filter.class.js +++ b/src/filters/invert_filter.class.js @@ -9,6 +9,11 @@ * @class fabric.Image.filters.Invert * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Invert = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { diff --git a/src/filters/mask_filter.class.js b/src/filters/mask_filter.class.js index dabacde5..f432cbed 100644 --- a/src/filters/mask_filter.class.js +++ b/src/filters/mask_filter.class.js @@ -11,6 +11,7 @@ * @class fabric.Image.filters.Mask * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Mask#initialize} for constructor definition */ fabric.Image.filters.Mask = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { @@ -85,25 +86,10 @@ * @param {Function} [callback] Callback to invoke when a mask filter instance is created */ fabric.Image.filters.Mask.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.mask.src; - - /** @ignore */ - img.onload = function() { + fabric.util.loadImage(object.mask.src, function(img) { object.mask = new fabric.Image(img, object.mask); - callback && callback(new fabric.Image.filters.Mask(object)); - img = img.onload = img.onerror = null; - }; - - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback(null, true); - img = img.onload = img.onerror = null; - }; - - img.src = src; + }); }; /** diff --git a/src/filters/noise_filter.class.js b/src/filters/noise_filter.class.js index a3a20c3b..b6900554 100644 --- a/src/filters/noise_filter.class.js +++ b/src/filters/noise_filter.class.js @@ -10,6 +10,14 @@ * @class fabric.Image.filters.Noise * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Noise = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { diff --git a/src/filters/pixelate_filter.class.js b/src/filters/pixelate_filter.class.js index 6f2cf47e..1c31420b 100644 --- a/src/filters/pixelate_filter.class.js +++ b/src/filters/pixelate_filter.class.js @@ -10,6 +10,14 @@ * @class fabric.Image.filters.Pixelate * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Pixelate = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { diff --git a/src/filters/removewhite_filter.class.js b/src/filters/removewhite_filter.class.js index d587fd01..1d33d994 100644 --- a/src/filters/removewhite_filter.class.js +++ b/src/filters/removewhite_filter.class.js @@ -10,6 +10,15 @@ * @class fabric.Image.filters.RemoveWhite * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveWhite#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveWhite({ + * threshold: 40, + * distance: 140 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.RemoveWhite = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ { diff --git a/src/filters/sepia2_filter.class.js b/src/filters/sepia2_filter.class.js index 0dbe45fb..c39ea090 100644 --- a/src/filters/sepia2_filter.class.js +++ b/src/filters/sepia2_filter.class.js @@ -9,6 +9,11 @@ * @class fabric.Image.filters.Sepia2 * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia2(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia2 = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { diff --git a/src/filters/sepia_filter.class.js b/src/filters/sepia_filter.class.js index 70f493e9..663d5b3c 100644 --- a/src/filters/sepia_filter.class.js +++ b/src/filters/sepia_filter.class.js @@ -9,6 +9,11 @@ * @class fabric.Image.filters.Sepia * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { diff --git a/src/filters/tint_filter.class.js b/src/filters/tint_filter.class.js index 6ab694c5..6a1c1093 100644 --- a/src/filters/tint_filter.class.js +++ b/src/filters/tint_filter.class.js @@ -11,6 +11,21 @@ * @class fabric.Image.filters.Tint * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Tint#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} + * @example Tint filter with hex color and opacity + * var filter = new fabric.Image.filters.Tint({ + * color: '#3513B0', + * opacity: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Tint filter with rgba color + * var filter = new fabric.Image.filters.Tint({ + * color: 'rgba(53, 21, 176, 0.5)' + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Tint = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { diff --git a/src/gradient.class.js b/src/gradient.class.js index 289546d5..0f0210f3 100644 --- a/src/gradient.class.js +++ b/src/gradient.class.js @@ -47,12 +47,33 @@ opacity: isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity) }; } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } /* _FROM_SVG_END_ */ /** * Gradient class * @class fabric.Gradient * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition */ fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { @@ -269,22 +290,10 @@ coords = { }; if (type === 'linear') { - coords = { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; + coords = getLinearCoords(el); } else if (type === 'radial') { - coords = { - x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', - y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', - r1: 0, - x2: el.getAttribute('cx') || '50%', - y2: el.getAttribute('cy') || '50%', - r2: el.getAttribute('r') || '50%' - }; + coords = getRadialCoords(el); } for (var i = colorStopEls.length; i--; ) { @@ -330,13 +339,17 @@ options[prop] = fabric.util.toFixed(object.height * percents / 100, 2); } } - // normalize rendering point (should be from top/left corner rather than center of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] -= fabric.util.toFixed(object.width / 2, 2); - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] -= fabric.util.toFixed(object.height / 2, 2); - } + normalize(options, prop, object); + } + } + + // normalize rendering point (should be from top/left corner rather than center of the shape) + function normalize(options, prop, object) { + if (prop === 'x1' || prop === 'x2') { + options[prop] -= fabric.util.toFixed(object.width / 2, 2); + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= fabric.util.toFixed(object.height / 2, 2); } } @@ -346,13 +359,9 @@ */ function _convertValuesToPercentUnits(object, options) { for (var prop in options) { - // normalize rendering point (should be from center rather than top/left corner of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] += fabric.util.toFixed(object.width / 2, 2); - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] += fabric.util.toFixed(object.height / 2, 2); - } + + normalize(options, prop, object); + // convert to percent units if (prop === 'x1' || prop === 'x2' || prop === 'r2') { options[prop] = fabric.util.toFixed(options[prop] / object.width * 100, 2) + '%'; diff --git a/src/log.js b/src/log.js index 65c4bda2..22d23763 100644 --- a/src/log.js +++ b/src/log.js @@ -11,14 +11,11 @@ fabric.log = function() { }; fabric.warn = function() { }; if (typeof console !== 'undefined') { - if (typeof console.log !== 'undefined' && console.log.apply) { - fabric.log = function() { - return console.log.apply(console, arguments); - }; - } - if (typeof console.warn !== 'undefined' && console.warn.apply) { - fabric.warn = function() { - return console.warn.apply(console, arguments); - }; - } + ['log', 'warn'].forEach(function(methodName) { + if (typeof console[methodName] !== 'undefined' && console[methodName].apply) { + fabric[methodName] = function() { + return console[methodName].apply(console, arguments); + }; + } + }); } diff --git a/src/mixins/canvas_dataurl_exporter.mixin.js b/src/mixins/canvas_dataurl_exporter.mixin.js index 4b952e24..fa3f327d 100644 --- a/src/mixins/canvas_dataurl_exporter.mixin.js +++ b/src/mixins/canvas_dataurl_exporter.mixin.js @@ -146,15 +146,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.deactivateAll(); } + this.renderAll(true); + + var data = this.__toDataURL(format, quality, cropping); + // restoring width, height for `renderAll` to draw // background properly (while context is scaled) this.width = origWidth; this.height = origHeight; - this.renderAll(true); - - var data = this.__toDataURL(format, quality, cropping); - ctx.scale(1 / multiplier, 1 / multiplier); this.setWidth(origWidth).setHeight(origHeight); diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 67d8babe..f53e8145 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -1,14 +1,14 @@ (function(){ var cursorMap = [ - 'n-resize', - 'ne-resize', - 'e-resize', - 'se-resize', - 's-resize', - 'sw-resize', - 'w-resize', - 'nw-resize' + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + 'nw-resize' ], cursorOffset = { 'mt': 0, // n @@ -30,34 +30,110 @@ * Adds mouse listeners to canvas * @private */ - _initEvents: function () { - var _this = this; + _initEventListeners: function () { + this._bindEvents(); + + addListener(fabric.window, 'resize', this._onResize); + + // mouse events + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); + + // touch events + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (typeof Event !== 'undefined' && 'add' in Event) { + Event.add(this.upperCanvasEl, 'gesture', this._onGesture); + Event.add(this.upperCanvasEl, 'drag', this._onDrag); + Event.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.add(this.upperCanvasEl, 'shake', this._onShake); + } + }, + + /** + * @private + */ + _bindEvents: function() { this._onMouseDown = this._onMouseDown.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseUp = this._onMouseUp.bind(this); this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + }, - this._onGesture = function(e, s) { - _this.__onTransformGesture(e, s); - }; + /** + * Removes all event listeners + */ + removeListeners: function() { + removeListener(fabric.window, 'resize', this._onResize); - addListener(fabric.window, 'resize', this._onResize); + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); - if (fabric.isTouchSupported) { - addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - if (typeof Event !== 'undefined' && 'add' in Event) { - Event.add(this.upperCanvasEl, 'gesture', this._onGesture); - } - } - else { - addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + if (typeof Event !== 'undefined' && 'remove' in Event) { + Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); + Event.remove(this.upperCanvasEl, 'drag', this._onDrag); + Event.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); + Event.remove(this.upperCanvasEl, 'shake', this._onShake); } }, + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, s) { + this.__onTransformGesture && this.__onTransformGesture(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, s) { + this.__onDrag && this.__onDrag(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js wheel event + * @param {Event} [self] Inner Event object + */ + _onMouseWheel: function(e, s) { + this.__onMouseWheel && this.__onMouseWheel(e, s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e,s) { + this.__onOrientationChange && this.__onOrientationChange(e,s); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e,s) { + this.__onShake && this.__onShake(e,s); + }, + /** * @private * @param {Event} e Event object fired on mousedown @@ -65,14 +141,14 @@ _onMouseDown: function (e) { this.__onMouseDown(e); - !fabric.isTouchSupported && addListener(fabric.document, 'mouseup', this._onMouseUp); - fabric.isTouchSupported && addListener(fabric.document, 'touchend', this._onMouseUp); + addListener(fabric.document, 'mouseup', this._onMouseUp); + addListener(fabric.document, 'touchend', this._onMouseUp); - !fabric.isTouchSupported && addListener(fabric.document, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && addListener(fabric.document, 'touchmove', this._onMouseMove); + addListener(fabric.document, 'mousemove', this._onMouseMove); + addListener(fabric.document, 'touchmove', this._onMouseMove); - !fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); }, /** @@ -82,14 +158,14 @@ _onMouseUp: function (e) { this.__onMouseUp(e); - !fabric.isTouchSupported && removeListener(fabric.document, 'mouseup', this._onMouseUp); - fabric.isTouchSupported && removeListener(fabric.document, 'touchend', this._onMouseUp); + removeListener(fabric.document, 'mouseup', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onMouseUp); - !fabric.isTouchSupported && removeListener(fabric.document, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', this._onMouseMove); + removeListener(fabric.document, 'mousemove', this._onMouseMove); + removeListener(fabric.document, 'touchmove', this._onMouseMove); - !fabric.isTouchSupported && addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - fabric.isTouchSupported && addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); }, /** @@ -117,16 +193,20 @@ _shouldRender: function(target, pointer) { var activeObject = this.getActiveGroup() || this.getActiveObject(); - return ( + return !!( (target && ( - target.isMoving || - target !== activeObject)) || - (!target && activeObject) || + target.isMoving || + target !== activeObject)) + || + (!target && !!activeObject) + || + (!target && !activeObject && !this._groupSelector) + || (pointer && - this._previousPointer && - this.selection && ( - pointer.x !== this._previousPointer.x || - pointer.y !== this._previousPointer.y)) + this._previousPointer && + this.selection && ( + pointer.x !== this._previousPointer.x || + pointer.y !== this._previousPointer.y)) ); }, @@ -138,84 +218,38 @@ * @param {Event} e Event object fired on mouseup */ __onMouseUp: function (e) { - var target, - pointer, - render; + var target; if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._isCurrentlyDrawing = false; - if (this.clipTo) { - this.contextTop.restore(); - } - this.freeDrawingBrush.onMouseUp(); - this.fire('mouse:up', { e: e }); + this._onMouseUpInDrawingMode(e); return; } if (this._currentTransform) { - - var transform = this._currentTransform; - - target = transform.target; - if (target._scaling) { - target._scaling = false; - } - - target.setCoords(); - - // only fire :modified event if target coordinates were changed during mousedown-mouseup - if (this.stateful && target.hasStateChanged()) { - this.fire('object:modified', { target: target }); - target.fire('modified'); - } - - if (this._previousOriginX && this._previousOriginY) { - - var originPoint = target.translateToOriginPoint( - target.getCenterPoint(), - this._previousOriginX, - this._previousOriginY); - - target.originX = this._previousOriginX; - target.originY = this._previousOriginY; - - target.left = originPoint.x; - target.top = originPoint.y; - - this._previousOriginX = null; - this._previousOriginY = null; - } + this._finalizeCurrentTransform(); + target = this._currentTransform.target; } else { - pointer = this.getPointer(e, true); + target = this.findTarget(e, true); } - render = this._shouldRender(target, pointer); + var shouldRender = this._shouldRender(target, this.getPointer(e)); - if (this.selection && this._groupSelector) { - // group selection was completed, determine its bounds - this._findSelectedObjects(e); - } - - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords(); - activeGroup.isMoving = false; - this._setCursor(this.defaultCursor); - } - - // clear selection and current transformation - this._groupSelector = null; - this._currentTransform = null; + this._maybeGroupObjects(e); if (target) { target.isMoving = false; } - render && this.renderAll(); + shouldRender && this.renderAll(); + this._handleCursorAndEvent(e, target); + }, + + _handleCursorAndEvent: function(e, target) { this._setCursorFromEvent(e, target); + // TODO: why are we doing this? var _this = this; setTimeout(function () { _this._setCursorFromEvent(e, target); @@ -225,13 +259,59 @@ target && target.fire('mouseup', { e: e }); }, + /** + * @private + */ + _finalizeCurrentTransform: function() { + + var transform = this._currentTransform; + var target = transform.target; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + + this._restoreOriginXY(target); + }, + + /** + * @private + * @param {Object} target Object to restore + */ + _restoreOriginXY: function(target) { + if (this._previousOriginX && this._previousOriginY) { + + var originPoint = target.translateToOriginPoint( + target.getCenterPoint(), + this._previousOriginX, + this._previousOriginY); + + target.originX = this._previousOriginX; + target.originY = this._previousOriginY; + + target.left = originPoint.x; + target.top = originPoint.y; + + this._previousOriginX = null; + this._previousOriginY = null; + } + }, + /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDownInDrawingMode: function(e) { this._isCurrentlyDrawing = true; - this.discardActiveObject().renderAll(); + this.discardActiveObject(e).renderAll(); if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } @@ -241,6 +321,33 @@ this.fire('mouse:down', { e: e }); }, + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var ivt = fabric.util.invertTransform(this.viewportTransform); + pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); + this.freeDrawingBrush.onMouseMove(pointer); + } + this.upperCanvasEl.style.cursor = this.freeDrawingCursor; + this.fire('mouse:move', { e: e }); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + this._isCurrentlyDrawing = false; + if (this.clipTo) { + this.contextTop.restore(); + } + this.freeDrawingBrush.onMouseUp(); + this.fire('mouse:up', { e: e }); + }, + /** * Method that defines the actions when mouse is clic ked on canvas. * The method inits the currentTransform parameters and renders all the @@ -250,6 +357,7 @@ * @param {Event} e Event object fired on mousedown */ __onMouseDown: function (e) { + // accept only left clicks var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; if (!isLeftClick && !fabric.isTouchSupported) return; @@ -263,53 +371,71 @@ if (this._currentTransform) return; var target = this.findTarget(e), - pointer = this.getPointer(e, true), - corner, - render; + pointer = this.getPointer(e, true); // save pointer for check in __onMouseUp event this._previousPointer = pointer; - render = this._shouldRender(target, pointer); + var shouldRender = this._shouldRender(target, pointer), + shouldGroup = this._shouldGroup(e, target); if (this._shouldClearSelection(e, target)) { - if (this.selection) { - this._groupSelector = { - ex: pointer.x, - ey: pointer.y, - top: 0, - left: 0 - }; - } - this.deactivateAllWithDispatch(); - target && target.selectable && this.setActiveObject(target, e); + this._clearSelection(e, target, pointer); } - else if (this._shouldHandleGroupLogic(e, target)) { - this._handleGroupLogic(e, target); + else if (shouldGroup) { + this._handleGrouping(e, target); target = this.getActiveGroup(); } - else { - // determine if it's a drag or rotate case - this.stateful && target.saveState(); - - if ((corner = target._findTargetCorner(e, this._offset))) { - this.onBeforeScaleRotate(target); - } - - if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { - this.deactivateAll(); - this.setActiveObject(target, e); - } + if (target && target.selectable && !shouldGroup) { + this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } // we must renderAll so that active image is placed on the top canvas - render && this.renderAll(); + shouldRender && this.renderAll(); this.fire('mouse:down', { target: target, e: e }); target && target.fire('mousedown', { e: e }); }, + /** + * @private + */ + _beforeTransform: function(e, target) { + var corner; + + this.stateful && target.saveState(); + + // determine if it's a drag or rotate case + if ((corner = target._findTargetCorner(e, this._offset))) { + this.onBeforeScaleRotate(target); + } + + if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { + this.deactivateAll(); + this.setActiveObject(target, e); + } + }, + + /** + * @private + */ + _clearSelection: function(e, target, pointer) { + this.deactivateAllWithDispatch(e); + + if (target && target.selectable) { + this.setActiveObject(target, e); + } + else if (this.selection) { + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + } + }, + /** * @private * @param {Object} target Object for that origin is set to center @@ -360,130 +486,144 @@ * @param {Event} e Event object fired on mousemove */ __onMouseMove: function (e) { + var target, pointer; if (this.isDrawingMode) { - if (this._isCurrentlyDrawing) { - var ivt = fabric.util.invertTransform(this.viewportTransform); - pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); - this.freeDrawingBrush.onMouseMove(pointer); - } - this.upperCanvasEl.style.cursor = this.freeDrawingCursor; - this.fire('mouse:move', { e: e }); + this._onMouseMoveInDrawingMode(e); return; } var groupSelector = this._groupSelector; - // We initially clicked in an empty area, so we draw a box for multiple selection. + // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { pointer = this.getPointer(e, true); - groupSelector.left = pointer.x - groupSelector.ex; - groupSelector.top = pointer.y - groupSelector.ey; + groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; + groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + this.renderTop(); } else if (!this._currentTransform) { - // alias style to elimintate unnecessary lookup - var style = this.upperCanvasEl.style; - - // Here we are hovering the canvas then we will determine - // what part of the pictures we are hovering to change the caret symbol. - // We won't do that while dragging or rotating in order to improve the - // performance. target = this.findTarget(e); if (!target || target && !target.selectable) { - // no target - set default cursor - style.cursor = this.defaultCursor; + this.upperCanvasEl.style.cursor = this.defaultCursor; } else { - // set proper cursor this._setCursorFromEvent(e, target); } } else { - // object is being transformed (scaled/rotated/moved/etc.) - pointer = fabric.util.transformPoint( - getPointer(e, this.upperCanvasEl), - fabric.util.invertTransform(this.viewportTransform) - ); - - var x = pointer.x, - y = pointer.y, - reset = false, - centerTransform, - transform = this._currentTransform; - - target = transform.target; - target.isMoving = true; - - if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { - centerTransform = this._shouldCenterTransform(e, target); - - // Switch from a normal resize to center-based - if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || - // Switch from center-based resize to normal one - (!centerTransform && transform.originX === 'center' && transform.originY === 'center') - ) { - this._resetCurrentTransform(e); - reset = true; - } - } - - if (transform.action === 'rotate') { - this._rotateObject(x, y); - - this.fire('object:rotating', { target: target, e: e }); - target.fire('rotating', { e: e }); - } - else if (transform.action === 'scale') { - // rotate object only if shift key is not pressed - // and if it is not a group we are transforming - if ((e.shiftKey || this.uniScaleTransform) && !target.get('lockUniScaling')) { - transform.currentAction = 'scale'; - this._scaleObject(x, y); - } - else { - // Switch from a normal resize to proportional - if (!reset && transform.currentAction === 'scale') { - this._resetCurrentTransform(e, target); - } - - transform.currentAction = 'scaleEqually'; - this._scaleObject(x, y, 'equally'); - } - - this.fire('object:scaling', { target: target, e: e }); - target.fire('scaling', { e: e }); - } - else if (transform.action === 'scaleX') { - this._scaleObject(x, y, 'x'); - - this.fire('object:scaling', { target: target, e: e}); - target.fire('scaling', { e: e }); - } - else if (transform.action === 'scaleY') { - this._scaleObject(x, y, 'y'); - - this.fire('object:scaling', { target: target, e: e}); - target.fire('scaling', { e: e }); - } - else { - this._translateObject(x, y); - - this.fire('object:moving', { target: target, e: e}); - target.fire('moving', { e: e }); - this._setCursor(this.moveCursor); - } - - this.renderAll(); + this._transformObject(e); } + this.fire('mouse:move', { target: target, e: e }); target && target.fire('mousemove', { e: e }); }, + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + + var pointer = fabric.util.transformPoint( + getPointer(e, this.upperCanvasEl), + fabric.util.invertTransform(this.viewportTransform) + ), + transform = this._currentTransform; + + transform.reset = false, + transform.target.isMoving = true; + + this._beforeScaleTransform(e, transform); + this._performTransformAction(e, transform, pointer); + + this.renderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + target = transform.target, + action = transform.action; + + if (action === 'rotate') { + this._rotateObject(x, y); + this._fire('rotating', target, e); + } + else if (action === 'scale') { + this._onScale(e, transform, x, y); + this._fire('scaling', target, e); + } + else if (action === 'scaleX') { + this._scaleObject(x, y, 'x'); + this._fire('scaling', target, e); + } + else if (action === 'scaleY') { + this._scaleObject(x, y, 'y'); + this._fire('scaling', target, e); + } + else { + this._translateObject(x, y); + this._fire('moving', target, e); + this._setCursor(this.moveCursor); + } + }, + + /** + * @private + */ + _fire: function(eventName, target, e) { + this.fire('object:' + eventName, { target: target, e: e}); + target.fire(eventName, { e: e }); + }, + + /** + * @private + */ + _beforeScaleTransform: function(e, transform) { + if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { + var centerTransform = this._shouldCenterTransform(e, transform.target); + + // Switch from a normal resize to center-based + if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || + // Switch from center-based resize to normal one + (!centerTransform && transform.originX === 'center' && transform.originY === 'center') + ) { + this._resetCurrentTransform(e); + transform.reset = true; + } + } + }, + + /** + * @private + */ + _onScale: function(e, transform, x, y) { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + if ((e.shiftKey || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { + transform.currentAction = 'scale'; + this._scaleObject(x, y); + } + else { + // Switch from a normal resize to proportional + if (!transform.reset && transform.currentAction === 'scale') { + this._resetCurrentTransform(e, transform.target); + } + + transform.currentAction = 'scaleEqually'; + this._scaleObject(x, y, 'equally'); + } + }, + /** * Sets the cursor depending on where the canvas is being hovered. * Note: very buggy in Opera @@ -491,9 +631,10 @@ * @param {Object} target Object that the mouse is hovering, if so. */ _setCursorFromEvent: function (e, target) { - var s = this.upperCanvasEl.style; - if (!target) { - s.cursor = this.defaultCursor; + var style = this.upperCanvasEl.style; + + if (!target || !target.selectable) { + style.cursor = this.defaultCursor; return false; } else { @@ -504,29 +645,47 @@ && target._findTargetCorner(e, this._offset); if (!corner) { - s.cursor = target.hoverCursor || this.hoverCursor; + style.cursor = target.hoverCursor || this.hoverCursor; } else { - if (corner in cursorOffset) { - var n = Math.round((target.getAngle() % 360) / 45); - if (n<0) { - n += 8; // full circle ahead - } - n += cursorOffset[corner]; - // normalize n to be from 0 to 7 - n %= 8; - s.cursor = cursorMap[n]; - } - else if (corner === 'mtr' && target.hasRotatingPoint) { - s.cursor = this.rotationCursor; - } - else { - s.cursor = this.defaultCursor; - return false; - } + this._setCornerCursor(corner, target); } } return true; + }, + + /** + * @private + */ + _setCornerCursor: function(corner, target) { + var style = this.upperCanvasEl.style; + + if (corner in cursorOffset) { + style.cursor = this._getRotatedCornerCursor(corner, target); + } + else if (corner === 'mtr' && target.hasRotatingPoint) { + style.cursor = this.rotationCursor; + } + else { + style.cursor = this.defaultCursor; + return false; + } + }, + + /** + * @private + */ + _getRotatedCornerCursor: function(corner, target) { + var n = Math.round((target.getAngle() % 360) / 45); + + if (n < 0) { + n += 8; // full circle ahead + } + n += cursorOffset[corner]; + // normalize n to be from 0 to 7 + n %= 8; + + return cursorMap[n]; } }); })(); diff --git a/src/mixins/canvas_gestures.mixin.js b/src/mixins/canvas_gestures.mixin.js index 2949a254..f615d293 100644 --- a/src/mixins/canvas_gestures.mixin.js +++ b/src/mixins/canvas_gestures.mixin.js @@ -28,6 +28,36 @@ this.fire('touch:gesture', {target: target, e: e, self: self}); }, + /** + * Method that defines actions when an Event.js drag is detected. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onDrag: function(e, self) { + this.fire('touch:drag', {e: e, self: self}); + }, + + /** + * Method that defines actions when an Event.js orientation event is detected. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onOrientationChange: function(e, self) { + this.fire('touch:orientation', {e: e, self: self}); + }, + + /** + * Method that defines actions when an Event.js shake event is detected. + * + * @param e Event object by Event.js + * @param self Event proxy object by Event.js + */ + __onShake: function(e, self) { + this.fire('touch:shake', {e: e, self: self}); + }, + /** * Scales an object by a factor * @param s {Number} The scale factor to apply to the current scale level diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js new file mode 100644 index 00000000..8fb945a2 --- /dev/null +++ b/src/mixins/canvas_grouping.mixin.js @@ -0,0 +1,196 @@ +(function(){ + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this.getActiveObject(); + return e.shiftKey && + (this.getActiveGroup() || (activeObject && activeObject !== target)) + && this.selection; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + + if (target === this.getActiveGroup()) { + + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + if (this.getActiveGroup()) { + this._updateActiveGroup(target, e); + } + else { + this._createActiveGroup(target, e); + } + + if (this._activeGroup) { + this._activeGroup.saveCoords(); + } + }, + + /** + * @private + */ + _updateActiveGroup: function(target, e) { + var activeGroup = this.getActiveGroup(); + + if (activeGroup.contains(target)) { + + activeGroup.removeWithUpdate(target); + this._resetObjectTransform(activeGroup); + target.set('active', false); + + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(e); + // activate last remaining object + this.setActiveObject(activeGroup.item(0)); + return; + } + } + else { + activeGroup.addWithUpdate(target); + this._resetObjectTransform(activeGroup); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.set('active', true); + }, + + /** + * @private + */ + _createActiveGroup: function(target, e) { + + if (this._activeObject && target !== this._activeObject) { + + var group = this._createGroup(target); + + this.setActiveGroup(group); + this._activeObject = null; + + this.fire('selection:created', { target: group, e: e }); + } + + target.set('active', true); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + + var objects = this.getObjects(); + + var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target); + + var groupObjects = isActiveLower + ? [ this._activeObject, target ] + : [ target, this._activeObject ]; + + return new fabric.Group(groupObjects, { + originX: 'center', + originY: 'center' + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(); + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + group = new fabric.Group(group.reverse(), { + originX: 'center', + originY: 'center' + }); + this.setActiveGroup(group, e); + group.saveCoords(); + this.fire('selection:created', { target: group }); + this.renderAll(); + } + }, + + /** + * @private + */ + _collectObjects: function() { + var group = [ ], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + isClick = x1 === x2 && y1 === y2; + + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) continue; + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || + currentObject.containsPoint(selectionX1Y1) || + currentObject.containsPoint(selectionX2Y2) + ) { + currentObject.set('active', true); + group.push(currentObject); + + // only add one object if it's a click + if (isClick) break; + } + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords().setCoords(); + activeGroup.isMoving = false; + this._setCursor(this.defaultCursor); + } + + // clear selection and current transformation + this._groupSelector = null; + this._currentTransform = null; + } + }); + +})(); diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index e5a4f8aa..86bb3ad8 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -50,72 +50,74 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var _this = this; this._enlivenObjects(serialized.objects, function () { - _this._setBgOverlayImages(serialized, callback); + _this._setBgOverlay(serialized, callback); }, reviver); return this; }, - _setBgOverlayImages: function(serialized, callback) { - + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { var _this = this, - backgroundPatternLoaded, - backgroundImageLoaded, - overlayImageLoaded; + loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } var cbIfLoaded = function () { - callback && backgroundImageLoaded && overlayImageLoaded && backgroundPatternLoaded && callback(); + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + _this.renderAll(); + callback && callback(); + } }; - if (serialized.backgroundImage) { - this.setBackgroundImage(serialized.backgroundImage, function() { + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); - _this.backgroundImageOpacity = serialized.backgroundImageOpacity; - _this.backgroundImageStretch = serialized.backgroundImageStretch; + cbIfLoaded(); + }, - _this.renderAll(); + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; - backgroundImageLoaded = true; + if (!value) { + loaded[property] = true; + return; + } - cbIfLoaded(); + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.Image.fromObject(value, function(img) { + _this[property] = img; + loaded[property] = true; + callback && callback(); }); } else { - backgroundImageLoaded = true; - } - - if (serialized.overlayImage) { - this.setOverlayImage(serialized.overlayImage, function() { - - _this.overlayImageLeft = serialized.overlayImageLeft || 0; - _this.overlayImageTop = serialized.overlayImageTop || 0; - - _this.renderAll(); - overlayImageLoaded = true; - - cbIfLoaded(); + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); }); } - else { - overlayImageLoaded = true; - } - - if (serialized.background) { - this.setBackgroundColor(serialized.background, function() { - - _this.renderAll(); - backgroundPatternLoaded = true; - - cbIfLoaded(); - }); - } - else { - backgroundPatternLoaded = true; - } - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) { - callback && callback(); - } }, /** @@ -170,9 +172,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Clones canvas instance * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children */ - clone: function (callback) { - var data = JSON.stringify(this); + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); this.cloneWithoutData(function(clone) { clone.loadFromJSON(data, function() { callback && callback(clone); diff --git a/src/mixins/collection.mixin.js b/src/mixins/collection.mixin.js index dc4df82f..b387e7f9 100644 --- a/src/mixins/collection.mixin.js +++ b/src/mixins/collection.mixin.js @@ -79,6 +79,21 @@ fabric.Collection = { return this; }, + /** + * Returns an array of children objects of this instance + * Type parameter introduced in 1.3.10 + * @param {String} [type] When specified, only objects of this type are returned + * @return {Array} + */ + getObjects: function(type) { + if (typeof type === 'undefined') { + return this._objects; + } + return this._objects.filter(function(o) { + return o.type === type; + }); + }, + /** * Returns object at specified index * @param {Number} index diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js new file mode 100644 index 00000000..41299b4f --- /dev/null +++ b/src/mixins/itext.svg_export.js @@ -0,0 +1,114 @@ +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * @private + */ + _setSVGTextLineText: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + if (!this.styles[lineIndex]) { + this.callSuper('_setSVGTextLineText', + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier); + } + else { + this._setSVGTextLineChars( + textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + } + }, + + /** + * @private + */ + _setSVGTextLineChars: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + + var yProp = lineIndex === 0 || this.useNative ? 'y' : 'dy', + chars = textLine.split(''), + charOffset = 0, + lineLeftOffset = this._getSVGLineLeftOffset(lineIndex), + lineTopOffset = this._getSVGLineTopOffset(lineIndex), + heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); + + for (var i = 0, len = chars.length; i < len; i++) { + var styleDecl = this.styles[lineIndex][i] || { }; + + textSpans.push( + this._createTextCharSpan( + chars[i], styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset)); + + var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); + + if (styleDecl.textBackgroundColor) { + textBgRects.push( + this._createTextCharBg( + styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset)); + } + + charOffset += charWidth; + } + }, + + /** + * @private + */ + _getSVGLineLeftOffset: function(lineIndex) { + return (this._boundaries && this._boundaries[lineIndex]) + ? fabric.util.toFixed(this._boundaries[lineIndex].left, 2) + : 0; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0; + for (var j = 0; j <= lineIndex; j++) { + lineTopOffset += this._getHeightOfLine(this.ctx, j); + } + return lineTopOffset - this.height / 2; + }, + + /** + * @private + */ + _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { + return [ + '' + ].join(''); + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset) { + + var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ + visible: true, + fill: this.fill, + stroke: this.stroke, + type: 'text' + }, styleDecl)); + + return [ + '', + + fabric.util.string.escapeXml(_char), + '' + ].join(''); + } +}); +/* _TO_SVG_END_ */ diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js new file mode 100644 index 00000000..24d48d07 --- /dev/null +++ b/src/mixins/itext_behavior.mixin.js @@ -0,0 +1,640 @@ +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initKeyHandlers(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.initHiddenTextarea(); + }, + + /** + * Initializes "selected" event handler + */ + initSelectedHandler: function() { + this.on('selected', function() { + + var _this = this; + setTimeout(function() { + _this.selected = true; + }, 100); + + if (!this._hasCanvasHandlers) { + this._initCanvasHandlers(); + this._hasCanvasHandlers = true; + } + }); + }, + + /** + * @private + */ + _initCanvasHandlers: function() { + var _this = this; + + this.canvas.on('selection:cleared', function(options) { + + // do not exit editing if event fired + // when clicking on an object again (in editing mode) + if (options.e && _this.canvas.containsPoint(options.e, _this)) return; + + _this.exitEditing(); + }); + + this.canvas.on('mouse:up', function() { + this.getObjects('i-text').forEach(function(obj) { + obj.__isMousedown = false; + }); + }); + }, + + /** + * @private + */ + _tick: function() { + + var _this = this; + + if (this._abortCursorAnimation) return; + + this.animate('_currentCursorOpacity', 1, { + + duration: this.cursorDuration, + + onComplete: function() { + _this._onTickComplete(); + }, + + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, + + /** + * @private + */ + _onTickComplete: function() { + if (this._abortCursorAnimation) return; + + var _this = this; + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this.animate('_currentCursorOpacity', 0, { + duration: this.cursorDuration / 2, + onComplete: function() { + _this._tick(); + }, + onChange: function() { + _this.canvas && _this.canvas.renderAll(); + }, + abort: function() { + return _this._abortCursorAnimation; + } + }); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function() { + var _this = this; + if (this._cursorTimeout2) { + clearTimeout(this._cursorTimeout2); + } + this._cursorTimeout2 = setTimeout(function() { + _this._abortCursorAnimation = false; + _this._tick(); + }, this.cursorDelay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + this._abortCursorAnimation = true; + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + this.canvas && this.canvas.renderAll(); + + var _this = this; + setTimeout(function() { + _this._abortCursorAnimation = false; + }, 10); + }, + + /** + * Selects entire text + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this.text.length; + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this.text.slice(this.selectionStart, this.selectionEnd); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Surrent selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index--; + } + } + while (/\S/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index++; + } + } + while (/\S/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} current selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} current selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Returns number of newlines in selected text + * @return {Number} Number of newlines in selected text + */ + getNumNewLinesInSelectedText: function() { + var selectedText = this.getSelectedText(); + var numNewLines = 0; + for (var i = 0, chars = selectedText.split(''), len = chars.length; i < len; i++) { + if (chars[i] === '\n') { + numNewLines++; + } + } + return numNewLines; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction: 1 or -1 + */ + searchWordBoundary: function(selectionStart, direction) { + var index = selectionStart; + var _char = this.text.charAt(index); + var reNonWord = /[ \n\.,;!\?\-]/; + + while (!reNonWord.test(_char) && index > 0 && index < this.text.length) { + index += direction; + _char = this.text.charAt(index); + } + if (reNonWord.test(_char) && _char !== '\n') { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + var newSelectionStart = this.searchWordBoundary(selectionStart, -1); /* search backwards */ + var newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + */ + selectLine: function(selectionStart) { + var newSelectionStart = this.findLineBoundaryLeft(selectionStart); + var newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionEnd); + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function() { + if (this.isEditing || !this.editable) return; + + this.exitEditingOnOthers(); + + this.isEditing = true; + + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + + this._tick(); + this.canvas && this.canvas.renderAll(); + + this.fire('editing:entered'); + + return this; + }, + + exitEditingOnOthers: function() { + fabric.IText.instances.forEach(function(obj) { + if (obj === this) return; + obj.exitEditing(); + }, this); + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * @private + */ + _updateTextarea: function() { + if (!this.hiddenTextarea) return; + + this.hiddenTextarea.value = this.text; + this.hiddenTextarea.selectionStart = this.selectionStart; + this.hiddenTextarea.focus(); + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) return; + + this.hoverCursor = this._savedProps.overCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + + this.selected = false; + this.isEditing = false; + this.selectable = true; + + this.selectionEnd = this.selectionStart; + this.hiddenTextarea && this.hiddenTextarea.blur(); + + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + + this.fire('editing:exited'); + + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + var textLines = this.text.split(this._reNewline); + for (var prop in this.styles) { + if (!textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * @private + */ + _removeCharsFromTo: function(start, end) { + + var i = end; + while (i !== start) { + + var prevIndex = this.get2DCursorLocation(i).charIndex; + i--; + var index = this.get2DCursorLocation(i).charIndex; + var isNewline = index > prevIndex; + + if (isNewline) { + this.removeStyleObject(isNewline, i + 1); + } + else { + this.removeStyleObject(this.get2DCursorLocation(i).charIndex === 0, i); + } + + } + + this.text = this.text.slice(0, start) + + this.text.slice(end); + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + * @param {String} _chars Characters to insert + */ + insertChars: function(_chars) { + var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n'; + + this.text = this.text.slice(0, this.selectionStart) + + _chars + + this.text.slice(this.selectionEnd); + + if (this.selectionStart === this.selectionEnd) { + this.insertStyleObject(_chars, isEndOfLine); + } + else if (this.selectionEnd - this.selectionStart > 1) { + // TODO: replace styles properly + // console.log('replacing MORE than 1 char'); + } + + this.selectionStart += _chars.length; + this.selectionEnd = this.selectionStart; + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('text:changed'); + }, + + /** + * Inserts new style object + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { + + this.shiftLineStyles(lineIndex, +1); + + if (!this.styles[lineIndex + 1]) { + this.styles[lineIndex + 1] = { }; + } + + var currentCharStyle = this.styles[lineIndex][charIndex - 1], + newLineStyles = { }; + + // if there's nothing after cursor, + // we clone current char style onto the next (otherwise empty) line + if (isEndOfLine) { + newLineStyles[0] = clone(currentCharStyle); + this.styles[lineIndex + 1] = newLineStyles; + } + // otherwise we clone styles of all chars + // after cursor onto the next line, from the beginning + else { + for (var index in this.styles[lineIndex]) { + if (parseInt(index, 10) >= charIndex) { + newLineStyles[parseInt(index, 10) - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + delete this.styles[lineIndex][index]; + } + } + this.styles[lineIndex + 1] = newLineStyles; + } + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + */ + insertCharStyleObject: function(lineIndex, charIndex) { + + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = clone(currentLineStyles); + + if (charIndex === 0) { + charIndex = 1; + } + + // shift all char styles by 1 forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex]; + //delete currentLineStyles[index]; + } + } + this.styles[lineIndex][charIndex] = clone(currentLineStyles[charIndex - 1]); + }, + + /** + * Inserts style object + * @param {String} _chars Characters at the location where style is inserted + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertStyleObject: function(_chars, isEndOfLine) { + + // short-circuit + if (this.isEmptyStyles()) return; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = { }; + } + + if (_chars === '\n') { + this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); + } + else { + // TODO: support multiple style insertion if _chars.length > 1 + this.insertCharStyleObject(lineIndex, charIndex); + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can be -1 or +1 + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by 1 upward + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + } + } + }, + + /** + * Removes style object + * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line + * @param {Number} [index] Optional index. When not given, current selectionStart is used. + */ + removeStyleObject: function(isBeginningOfLine, index) { + + var cursorLocation = this.get2DCursorLocation(index), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (isBeginningOfLine) { + + var textLines = this.text.split(this._reNewline), + textOnPreviousLine = textLines[lineIndex - 1], + newCharIndexOnPrevLine = textOnPreviousLine.length; + + if (!this.styles[lineIndex - 1]) { + this.styles[lineIndex - 1] = { }; + } + + for (charIndex in this.styles[lineIndex]) { + this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine] + = this.styles[lineIndex][charIndex]; + } + + this.shiftLineStyles(lineIndex, -1); + } + else { + var currentLineStyles = this.styles[lineIndex]; + + if (currentLineStyles) { + var offset = this.selectionStart === this.selectionEnd ? -1 : 0; + delete currentLineStyles[charIndex + offset]; + // console.log('deleting', lineIndex, charIndex + offset); + } + + var currentLineStylesCloned = clone(currentLineStyles); + + // shift all styles by 1 backwards + for (var i in currentLineStylesCloned) { + var numericIndex = parseInt(i, 10); + if (numericIndex >= charIndex && numericIndex !== 0) { + currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex]; + delete currentLineStyles[numericIndex]; + } + } + } + }, + + /** + * Inserts new line + */ + insertNewline: function() { + this.insertChars('\n'); + } + }); +})(); diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js new file mode 100644 index 00000000..15d2ffd1 --- /dev/null +++ b/src/mixins/itext_click_behavior.mixin.js @@ -0,0 +1,265 @@ +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.lastPointer = { }; + + this.on('mousedown', this.onMouseDown.bind(this)); + }, + + onMouseDown: function(options) { + + this.__newClickTime = +new Date(); + var newPointer = this.canvas.getPointer(options.e); + + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + else if (this.isDoubleClick(newPointer)) { + this.fire('dblclick', options); + this._stopEvent(options.e); + } + + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + }, + + isDoubleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + /** + * @private + */ + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, + + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initSelectedHandler(); + this.initMousedownHandler(); + this.initMousemoveHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('dblclick', function(options) { + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }); + this.on('tripleclick', function(options) { + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }); + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', function(options) { + + var pointer = this.canvas.getPointer(options.e); + + this.__mousedownX = pointer.x; + this.__mousedownY = pointer.y; + this.__isMousedown = true; + + if (this.hiddenTextarea && this.canvas) { + this.canvas.wrapperEl.appendChild(this.hiddenTextarea); + } + + if (this.isEditing) { + this.setCursorByClick(options.e); + this.__selectionStartOnMouseDown = this.selectionStart; + } + else { + this.exitEditingOnOthers(); + } + }); + }, + + /** + * Initializes "mousemove" event handler + */ + initMousemoveHandler: function() { + this.on('mousemove', function(options) { + if (!this.__isMousedown || !this.isEditing) return; + + var newSelectionStart = this.getSelectionStartFromPointer(options.e); + + if (newSelectionStart >= this.__selectionStartOnMouseDown) { + this.setSelectionStart(this.__selectionStartOnMouseDown); + this.setSelectionEnd(newSelectionStart); + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(this.__selectionStartOnMouseDown); + } + }); + }, + + /** + * @private + */ + _isObjectMoved: function(e) { + var pointer = this.canvas.getPointer(e); + + return this.__mousedownX !== pointer.x || + this.__mousedownY !== pointer.y; + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', function(options) { + this.__isMousedown = false; + + if (this._isObjectMoved(options.e)) return; + + if (this.selected) { + this.enterEditing(); + } + }); + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Object} pointer Pointer object with x and y numeric properties + */ + setCursorByClick: function(e) { + var newSelectionStart = this.getSelectionStartFromPointer(e); + + if (e.shiftKey) { + if (newSelectionStart < this.selectionStart) { + this.setSelectionEnd(this.selectionStart); + this.setSelectionStart(newSelectionStart); + } + else { + this.setSelectionEnd(newSelectionStart); + } + } + else { + this.setSelectionStart(newSelectionStart); + this.setSelectionEnd(newSelectionStart); + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {Object} Object with x/y corresponding to local offset (according to object rotation) + */ + _getLocalRotatedPointer: function(e) { + var pointer = this.canvas.getPointer(e), + + pClicked = new fabric.Point(pointer.x, pointer.y), + pLeftTop = new fabric.Point(this.left, this.top), + + rotated = fabric.util.rotatePoint( + pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle)); + + return this.getLocalPointer(e, rotated); + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + + var mouseOffset = this._getLocalRotatedPointer(e), + textLines = this.text.split(this._reNewline), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + newSelectionStart; + + for (var i = 0, len = textLines.length; i < len; i++) { + + height += this._getHeightOfLine(this.ctx, i) * this.scaleY; + + var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfLine); + + width = lineLeftOffset; + + if (this.flipX) { + // when oject is horizontally flipped we reverse chars + textLines[i] = textLines[i].split('').reverse().join(''); + } + + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + + var _char = textLines[i][j]; + prevWidth = width; + + width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) * + this.scaleX; + + if (height <= mouseOffset.y || width <= mouseOffset.x) { + charIndex++; + continue; + } + + return this._getNewSelectionStartFromOffset( + mouseOffset, prevWidth, width, charIndex + i, jlen); + } + } + + // clicked somewhere after all chars, so set at the end + if (typeof newSelectionStart === 'undefined') { + return this.text.length; + } + }, + + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1, + newSelectionStart = index + offset; + + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this.text.length) { + newSelectionStart = this.text.length; + } + + return newSelectionStart; + } +}); diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js new file mode 100644 index 00000000..a465888f --- /dev/null +++ b/src/mixins/itext_key_behavior.mixin.js @@ -0,0 +1,605 @@ +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes key handlers + */ + initKeyHandlers: function() { + fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(fabric.document, 'keypress', this.onKeyPress.bind(this)); + }, + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px'; + + fabric.document.body.appendChild(this.hiddenTextarea); + }, + + /** + * @private + */ + _keysMap: { + 8: 'removeChars', + 13: 'insertNewline', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + 46: 'forwardDelete' + }, + + /** + * @private + */ + _ctrlKeysMap: { + 65: 'selectAll', + 67: 'copy', + 86: 'paste', + 88: 'cut' + }, + + /** + * Handles keyup event + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) return; + + if (e.keyCode in this._keysMap) { + this[this._keysMap[e.keyCode]](e); + } + else if ((e.keyCode in this._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) { + this[this._ctrlKeysMap[e.keyCode]](e); + } + else { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.canvas && this.canvas.renderAll(); + }, + + /** + * Forward delete + */ + forwardDelete: function(e) { + if (this.selectionStart === this.selectionEnd) { + this.moveCursorRight(e); + } + this.removeChars(e); + }, + + /** + * Copies selected text + */ + copy: function() { + var selectedText = this.getSelectedText(); + this.copiedText = selectedText; + }, + + /** + * Pastes text + */ + paste: function() { + if (this.copiedText) { + this.insertChars(this.copiedText); + } + }, + + /** + * Cuts text + */ + cut: function(e) { + this.copy(); + this.removeChars(e); + }, + + /** + * Handles keypress event + * @param {Event} e Event object + */ + onKeyPress: function(e) { + if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) { + return; + } + + this.insertChars(String.fromCharCode(e.which)); + + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * Gets start offset of a selection + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset, + + textBeforeCursor = this.text.slice(0, selectionProp), + textAfterCursor = this.text.slice(selectionProp), + + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1], + textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '', + + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on last line, down cursor goes to end of line + if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey) { + + // move to the end of a text + return this.text.length - selectionProp; + } + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnNextLine = this._getIndexOnNextLine( + cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnSameLineAfterCursor.length + 1 + indexOnNextLine; + }, + + /** + * @private + */ + _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex + 1; + var widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfNextLine); + var widthOfCharsOnNextLine = lineLeftOffset; + var indexOnNextLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) { + + var _char = textOnNextLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnNextLine += widthOfChar; + + if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnNextLine - widthOfChar; + var rightEdge = widthOfCharsOnNextLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j; + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnNextLine = textOnNextLine.length; + } + + return indexOnNextLine; + }, + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorDownWithShift(offset); + } + else { + this.moveCursorDownWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor down without keeping selection + * @param {Number} offset + */ + moveCursorDownWithoutShift: function(offset) { + + this._selectionDirection = 'right'; + this.selectionStart += offset; + + if (this.selectionStart > this.text.length) { + this.selectionStart = this.text.length; + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor down while keeping selection + * @param {Number} offset + */ + moveCursorDownWithShift: function(offset) { + + if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) { + this.selectionStart += offset; + this._selectionDirection = 'left'; + return; + } + else { + this._selectionDirection = 'right'; + this.selectionEnd += offset; + + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + getUpCursorOffset: function(e, isRight) { + + var selectionProp = isRight ? this.selectionEnd : this.selectionStart, + cursorLocation = this.get2DCursorLocation(selectionProp); + + // if on first line, up cursor goes to start of line + if (cursorLocation.lineIndex === 0 || e.metaKey) { + return selectionProp; + } + + var textBeforeCursor = this.text.slice(0, selectionProp), + textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), + textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '', + textLines = this.text.split(this._reNewline), + _char, + lineLeftOffset; + + var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); + + var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset; + var lineIndex = cursorLocation.lineIndex; + + for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { + _char = textOnSameLineBeforeCursor[i]; + widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + + var indexOnPrevLine = this._getIndexOnPrevLine( + cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines); + + return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length; + }, + + /** + * @private + */ + _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + + var lineIndex = cursorLocation.lineIndex - 1; + var widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines); + var lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine); + var widthOfCharsOnPreviousLine = lineLeftOffset; + var indexOnPrevLine = 0; + var foundMatch; + + for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) { + + var _char = textOnPreviousLine[j]; + var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnPreviousLine += widthOfChar; + + if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnPreviousLine - widthOfChar; + var rightEdge = widthOfCharsOnPreviousLine; + var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor); + var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); + + indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnPrevLine = textOnPreviousLine.length - 1; + } + + return indexOnPrevLine; + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right'); + + if (e.shiftKey) { + this.moveCursorUpWithShift(offset); + } + else { + this.moveCursorUpWithoutShift(offset); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor up with shift + * @param {Number} offset + */ + moveCursorUpWithShift: function(offset) { + + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + else { + if (this._selectionDirection === 'right') { + this.selectionEnd -= offset; + this._selectionDirection = 'right'; + return; + } + else { + this.selectionStart -= offset; + } + } + + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorUpWithoutShift: function(offset) { + if (this.selectionStart === this.selectionEnd) { + this.selectionStart -= offset; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + this.selectionEnd = this.selectionStart; + + this._selectionDirection = 'left'; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorLeftWithShift(e); + } + else { + this.moveCursorLeftWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * @private + */ + _move: function(e, prop, direction) { + if (e.altKey) { + this[prop] = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey) { + this[prop] = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += (direction === 'Left' ? -1 : 1); + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart) { + this._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + this._moveLeft(e, 'selectionEnd'); + } + else { + this._selectionDirection = 'left'; + this._moveLeft(e, 'selectionStart'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionStart) === '\n') { + this.selectionStart--; + } + if (this.selectionStart < 0) { + this.selectionStart = 0; + } + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + this.moveCursorRightWithShift(e); + } + else { + this.moveCursorRightWithoutShift(e); + } + + this.initDelayedCursor(); + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + } + else { + this._selectionDirection = 'right'; + this._moveRight(e, 'selectionEnd'); + + // increase selection by one if it's a newline + if (this.text.charAt(this.selectionEnd - 1) === '\n') { + this.selectionEnd++; + } + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e + */ + moveCursorRightWithoutShift: function(e) { + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += this.getNumNewLinesInSelectedText(); + if (this.selectionEnd > this.text.length) { + this.selectionEnd = this.text.length; + } + this.selectionStart = this.selectionEnd; + } + }, + + /** + * Inserts a character where cursor is (replacing selection if one exists) + */ + removeChars: function(e) { + if (this.selectionStart === this.selectionEnd) { + this._removeCharsNearCursor(e); + } + else { + this._removeCharsFromTo(this.selectionStart, this.selectionEnd); + } + + this.selectionEnd = this.selectionStart; + + this._removeExtraneousStyles(); + + if (this.canvas) { + // TODO: double renderAll gets rid of text box shift happenning sometimes + // need to find out what exactly causes it and fix it + this.canvas.renderAll().renderAll(); + } + + this.setCoords(); + this.fire('text:changed'); + }, + + /** + * @private + */ + _removeCharsNearCursor: function(e) { + if (this.selectionStart !== 0) { + + if (e.metaKey) { + // remove all till the start of current line + var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftLineBoundary, this.selectionStart); + this.selectionStart = leftLineBoundary; + } + else if (e.altKey) { + // remove all till the start of current word + var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftWordBoundary, this.selectionStart); + this.selectionStart = leftWordBoundary; + } + else { + var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n'; + this.removeStyleObject(isBeginningOfLine); + + this.selectionStart--; + this.text = this.text.slice(0, this.selectionStart) + + this.text.slice(this.selectionStart + 1); + } + } + } +}); diff --git a/src/mixins/object.svg_export.js b/src/mixins/object.svg_export.js new file mode 100644 index 00000000..39cc07f2 --- /dev/null +++ b/src/mixins/object.svg_export.js @@ -0,0 +1,95 @@ +/* _TO_SVG_START_ */ +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns styles-string for svg-export + * @return {String} + */ + getSvgStyles: function() { + + var fill = this.fill + ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) + : 'none'; + + var stroke = this.stroke + ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) + : 'none'; + + var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; + var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; + var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; + var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; + var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; + var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; + + var visibility = this.visible ? '' : " visibility: hidden;"; + var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + + return [ + "stroke: ", stroke, "; ", + "stroke-width: ", strokeWidth, "; ", + "stroke-dasharray: ", strokeDashArray, "; ", + "stroke-linecap: ", strokeLineCap, "; ", + "stroke-linejoin: ", strokeLineJoin, "; ", + "stroke-miterlimit: ", strokeMiterLimit, "; ", + "fill: ", fill, "; ", + "opacity: ", opacity, ";", + filter, + visibility + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @return {String} + */ + getSvgTransform: function() { + var toFixed = fabric.util.toFixed; + var angle = this.getAngle(); + var center = this.getCenterPoint(); + + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var translatePart = "translate(" + + toFixed(center.x, NUM_FRACTION_DIGITS) + + " " + + toFixed(center.y, NUM_FRACTION_DIGITS) + + ")"; + + var anglePart = angle !== 0 + ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") + : ''; + + var scalePart = (this.scaleX === 1 && this.scaleY === 1) + ? '' : + (" scale(" + + toFixed(this.scaleX, NUM_FRACTION_DIGITS) + + " " + + toFixed(this.scaleY, NUM_FRACTION_DIGITS) + + ")"); + + var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; + var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; + + return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function() { + var markup = [ ]; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + if (this.shadow) { + markup.push(this.shadow.toSVG(this)); + } + return markup; + } +}); +/* _TO_SVG_END_ */ diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index c929a437..937cce35 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -90,7 +90,7 @@ /** * Checks if point is inside the object - * @param {Object} point + * @param {fabric.Point} point Point to check against * @return {Boolean} true if point is inside the object */ containsPoint: function(point) { @@ -131,8 +131,8 @@ * Helper method to determine how many cross points are between the 4 object edges * and the horizontal line determined by a point on canvas * @private - * @param {Object} point - * @param {Object} oCoords Coordinates of the image being evaluated + * @param {fabric.Point} point Point to check + * @param {Object} oCoords Coordinates of the object being evaluated */ _findCrossPoints: function(point, oCoords) { var b1, b2, a1, a2, xi, yi, diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index c5214f74..9ac2b341 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -1,12 +1,19 @@ (function(){ var getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians; + degreesToRadians = fabric.util.degreesToRadians, + isVML = typeof G_vmlCanvasManager !== 'undefined'; fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** - * Determines which one of the four corners has been clicked + * The object interactivity controls. + * @private + */ + _controlsVisibility: null, + + /** + * Determines which corner has been clicked * @private * @param {Event} e Event object * @param {Object} offset Canvas offset @@ -23,6 +30,10 @@ for (var i in this.oCoords) { + if (!this.isControlVisible(i)) { + continue; + } + if (i === 'mtr' && !this.hasRotatingPoint) { continue; } @@ -286,7 +297,7 @@ ~~(h + padding2 + strokeWidth * sy) + 1 ); - if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) { + if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY @@ -329,9 +340,7 @@ padding = this.padding, scaleOffset = size2, scaleOffsetSize = size2 - size, - methodName = this.transparentCorners ? 'strokeRect' : 'fillRect', - transparent = this.transparentCorners, - isVML = typeof G_vmlCanvasManager !== 'undefined'; + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; ctx.save(); @@ -341,78 +350,139 @@ ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left - _left = left - scaleOffset - strokeWidth2 - padding; - _top = top - scaleOffset - strokeWidth2 - padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('tl', ctx, methodName, + left - scaleOffset - strokeWidth2 - padding, + top - scaleOffset - strokeWidth2 - padding); // top-right - _left = left + width - scaleOffset + strokeWidth2 + padding; - _top = top - scaleOffset - strokeWidth2 - padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('tr', ctx, methodName, + left + width - scaleOffset + strokeWidth2 + padding, + top - scaleOffset - strokeWidth2 - padding); // bottom-left - _left = left - scaleOffset - strokeWidth2 - padding; - _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('tr', ctx, methodName, + left - scaleOffset - strokeWidth2 - padding, + top + height + scaleOffsetSize + strokeWidth2 + padding); // bottom-right - _left = left + width + scaleOffsetSize + strokeWidth2 + padding; - _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('br', ctx, methodName, + left + width + scaleOffsetSize + strokeWidth2 + padding, + top + height + scaleOffsetSize + strokeWidth2 + padding); if (!this.get('lockUniScaling')) { - // middle-top - _left = left + width/2 - scaleOffset; - _top = top - scaleOffset - strokeWidth2 - padding; - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + // middle-top + this._drawControl('mt', ctx, methodName, + left + width/2 - scaleOffset, + top - scaleOffset - strokeWidth2 - padding); // middle-bottom - _left = left + width/2 - scaleOffset; - _top = top + height + scaleOffsetSize + strokeWidth2 + padding; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('mb', ctx, methodName, + left + width/2 - scaleOffset, + top + height + scaleOffsetSize + strokeWidth2 + padding); // middle-right - _left = left + width + scaleOffsetSize + strokeWidth2 + padding; - _top = top + height/2 - scaleOffset; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('mb', ctx, methodName, + left + width + scaleOffsetSize + strokeWidth2 + padding, + top + height/2 - scaleOffset); // middle-left - _left = left - scaleOffset - strokeWidth2 - padding; - _top = top + height/2 - scaleOffset; - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('ml', ctx, methodName, + left - scaleOffset - strokeWidth2 - padding, + top + height/2 - scaleOffset); } // middle-top-rotate if (this.hasRotatingPoint) { - - _left = left + width/2 - scaleOffset; - _top = this.flipY ? - (top + height + (this.rotatingPointOffset) - size2 + strokeWidth2 + padding) - : (top - (this.rotatingPointOffset) - size2 - strokeWidth2 - padding); - - isVML || transparent || ctx.clearRect(_left, _top, size, size); - ctx[methodName](_left, _top, size, size); + this._drawControl('mtr', ctx, methodName, + left + width/2 - scaleOffset, + this.flipY + ? (top + height + this.rotatingPointOffset - this.cornerSize/2 + strokeWidth2 + padding) + : (top - this.rotatingPointOffset - this.cornerSize/2 - strokeWidth2 - padding)); } ctx.restore(); return this; + }, + + /** + * @private + */ + _drawControl: function(control, ctx, methodName, left, top) { + var size = this.cornerSize; + + if (this.isControlVisible(control)) { + isVML || this.transparentCorners || ctx.clearRect(left, top, size, size); + ctx[methodName](left, top, size, size); + } + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlName) { + return this._getControlsVisibility()[controlName]; + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlName, visible) { + this._getControlsVisibility()[controlName] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + /** + * Returns the instance of the control visibility set for this object. + * @private + * @returns {Object} + */ + _getControlsVisibility: function() { + if (!this._controlsVisibility) { + this._controlsVisibility = { + tl: true, + tr: true, + br: true, + bl: true, + ml: true, + mt: true, + mr: true, + mb: true, + mtr: true + }; + } + return this._controlsVisibility; } }); })(); diff --git a/src/mixins/object_origin.mixin.js b/src/mixins/object_origin.mixin.js index 543ae2c9..102b170a 100644 --- a/src/mixins/object_origin.mixin.js +++ b/src/mixins/object_origin.mixin.js @@ -12,20 +12,22 @@ * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { - var cx = point.x, cy = point.y; + var cx = point.x, + cy = point.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; - if ( originX === "left" ) { - cx = point.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) )/ 2; + if (originX === "left") { + cx = point.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - cx = point.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + else if (originX === "right") { + cx = point.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - if ( originY === "top" ) { - cy = point.y +( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2; + if (originY === "top") { + cy = point.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - cy = point.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2; + else if (originY === "bottom") { + cy = point.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the reverse rotation to the point (it's already scaled properly) @@ -40,20 +42,22 @@ * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { - var x = center.x, y = center.y; + var x = center.x, + y = center.y, + strokeWidth = this.stroke ? this.strokeWidth : 0; // Get the point coordinates - if ( originX === "left" ) { - x = center.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - x = center.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2; + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } - if ( originY === "top" ) { - y = center.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2; + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - y = center.y + ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2; + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the rotation to the point (it's already scaled properly) @@ -91,29 +95,32 @@ /** * Returns the point in local coordinates - * @param {fabric.Point} The point relative to the global coordinate system + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ toLocalPoint: function(point, originX, originY) { - var center = this.getCenterPoint(); + var center = this.getCenterPoint(), + strokeWidth = this.stroke ? this.strokeWidth : 0, + x, y; - var x, y; - if (originX !== undefined && originY !== undefined) { - if ( originX === "left" ) { - x = center.x - (this.getWidth() + this.strokeWidth*this.scaleX) / 2; + if (originX && originY) { + if (originX === "left") { + x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } - else if ( originX === "right" ) { - x = center.x + (this.getWidth() + this.strokeWidth*this.scaleX)/ 2; + else if (originX === "right") { + x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } else { x = center.x; } - if ( originY === "top" ) { - y = center.y - (this.getHeight() + this.strokeWidth*this.scaleY) / 2; + if (originY === "top") { + y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } - else if ( originY === "bottom" ) { - y = center.y + (this.getHeight() + this.strokeWidth*this.scaleY)/ 2; + else if (originY === "bottom") { + y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } else { y = center.y; @@ -153,7 +160,7 @@ }, /** - * @param {String} to One of left, center, right + * @param {String} to One of 'left', 'center', 'right' */ adjustPosition: function(to) { var angle = degreesToRadians(this.angle); diff --git a/src/mixins/object_stacking.mixin.js b/src/mixins/object_stacking.mixin.js new file mode 100644 index 00000000..4124e1ee --- /dev/null +++ b/src/mixins/object_stacking.mixin.js @@ -0,0 +1,80 @@ +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group) { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else { + this.canvas.moveTo(this, index); + } + return this; + } +}); diff --git a/src/mixins/observable.mixin.js b/src/mixins/observable.mixin.js index 9158cf07..9c7774e0 100644 --- a/src/mixins/observable.mixin.js +++ b/src/mixins/observable.mixin.js @@ -46,7 +46,8 @@ } /** - * Stops event observing for a particular event handler + * Stops event observing for a particular event handler. Calling this method + * without arguments removes all handlers for all events * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead) * @memberOf fabric.Observable * @alias off @@ -58,8 +59,12 @@ function stopObserving(eventName, handler) { if (!this.__eventListeners) return; + // remove all key/value pairs (event name -> event handler) + if (arguments.length === 0) { + this.__eventListeners = { }; + } // one object with key/value pairs was passed - if (arguments.length === 1 && typeof arguments[0] === 'object') { + else if (arguments.length === 1 && typeof arguments[0] === 'object') { for (var prop in eventName) { _removeEventListener.call(this, prop, eventName[prop]); } diff --git a/src/node.js b/src/node.js index 962b36ba..4bb44fab 100644 --- a/src/node.js +++ b/src/node.js @@ -87,6 +87,9 @@ else if (url) { request(url, 'binary', createImageAndCallBack); } + else { + callback && callback.call(context, url); + } }; fabric.loadSVGFromURL = function(url, callback, reviver) { diff --git a/src/parser.js b/src/parser.js index d8fe57cf..7b5172b3 100644 --- a/src/parser.js +++ b/src/parser.js @@ -14,13 +14,6 @@ toFixed = fabric.util.toFixed, multiplyTransformMatrices = fabric.util.multiplyTransformMatrices; - fabric.SHARED_ATTRIBUTES = [ - "transform", - "fill", "fill-opacity", "fill-rule", - "opacity", - "stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width" - ]; - var attributesMap = { 'fill-opacity': 'fillOpacity', 'fill-rule': 'fillRule', @@ -105,55 +98,13 @@ return attributes; } - /** - * Returns an object of attributes' name/value, given element and an array of attribute names; - * Parses parent "g" nodes recursively upwards. - * @static - * @memberOf fabric - * @param {DOMElement} element Element to parse - * @param {Array} attributes Array of attributes to parse - * @return {Object} object containing parsed attributes' names/values - */ - function parseAttributes(element, attributes) { - - if (!element) { - return; - } - - var value, - parentAttributes = { }; - - // if there's a parent container (`g` node), parse its attributes recursively upwards - if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { - parentAttributes = fabric.parseAttributes(element.parentNode, attributes); - } - - var ownAttributes = attributes.reduce(function(memo, attr) { - value = element.getAttribute(attr); - if (value) { - attr = normalizeAttr(attr); - value = normalizeValue(attr, value, parentAttributes); - - memo[attr] = value; - } - return memo; - }, { }); - - // add values parsed from style, which take precedence over attributes - // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) - - ownAttributes = extend(ownAttributes, - extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); - return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); - } - /** * Parses "transform" attribute, returning an array of values * @static * @function * @memberOf fabric - * @param attributeValue {String} string containing attribute value - * @return {Array} array of 6 elements representing transformation matrix + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { @@ -200,13 +151,22 @@ // == begin transform regexp number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)', + comma_wsp = '(?:\\s+,?\\s*|,\\s*)', skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', - rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))', - scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', - translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + ')' + + comma_wsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + comma_wsp + '(' + number + '))?\\s*\\))', matrix = '(?:(matrix)\\s*\\(\\s*' + '(' + number + ')' + comma_wsp + @@ -292,49 +252,6 @@ }; })(); - /** - * Parses "points" attribute, returning an array of values - * @static - * @memberOf fabric - * @param points {String} points attribute string - * @return {Array} array of points - */ - function parsePointsAttribute(points) { - - // points attribute is required and must not be empty - if (!points) return null; - - points = points.trim(); - var asPairs = points.indexOf(',') > -1; - - points = points.split(/\s+/); - var parsedPoints = [ ], i, len; - - // points could look like "10,20 30,40" or "10 20 30 40" - if (asPairs) { - i = 0; - len = points.length; - for (; i < len; i++) { - var pair = points[i].split(','); - parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); - } - } - else { - i = 0; - len = points.length; - for (; i < len; i+=2) { - parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); - } - } - - // odd number of points is an error - if (parsedPoints.length % 2 !== 0) { - // return null; - } - - return parsedPoints; - } - function parseFontDeclaration(value, oStyle) { // TODO: support non-px font size @@ -368,166 +285,43 @@ } /** - * Parses "style" attribute, retuning an object with values - * @static - * @memberOf fabric - * @param {SVGElement} element Element to parse - * @return {Object} Objects with values parsed from style attribute of an element + * @private */ - function parseStyleAttribute(element) { - var oStyle = { }, - style = element.getAttribute('style'), - attr, value; + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); - if (!style) return oStyle; + attr = normalizeAttr(pair[0].trim().toLowerCase()); + value = normalizeValue(attr, pair[1].trim()); - if (typeof style === 'string') { - style.replace(/;$/, '').split(';').forEach(function (chunk) { - var pair = chunk.split(':'); - - attr = normalizeAttr(pair[0].trim().toLowerCase()); - value = normalizeValue(attr, pair[1].trim()); - - if (attr === 'font') { - parseFontDeclaration(value, oStyle); - } - else { - oStyle[attr] = value; - } - }); - } - else { - for (var prop in style) { - if (typeof style[prop] === 'undefined') continue; - - attr = normalizeAttr(prop.toLowerCase()); - value = normalizeValue(attr, style[prop]); - - if (attr === 'font') { - parseFontDeclaration(value, oStyle); - } - else { - oStyle[attr] = value; - } - } - } - - return oStyle; - } - - function resolveGradients(instances) { - for (var i = instances.length; i--; ) { - var instanceFillValue = instances[i].get('fill'); - - if (/^url\(/.test(instanceFillValue)) { - - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - - if (fabric.gradientDefs[gradientId]) { - instances[i].set('fill', - fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); - } - } - } - } - - /** - * Transforms an array of svg elements to corresponding fabric.* instances - * @static - * @memberOf fabric - * @param {Array} elements Array of elements to parse - * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) - * @param {Object} [options] Options object - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function parseElements(elements, callback, options, reviver) { - var instances = new Array(elements.length), i = elements.length; - - function checkIfDone() { - if (--i === 0) { - instances = instances.filter(function(el) { - return el != null; - }); - resolveGradients(instances); - callback(instances); - } - } - - for (var index = 0, el, len = elements.length; index < len; index++) { - el = elements[index]; - var klass = fabric[capitalize(el.tagName)]; - if (klass && klass.fromElement) { - try { - if (klass.async) { - klass.fromElement(el, (function(index, el) { - return function(obj) { - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - }; - })(index, el), options); - } - else { - var obj = klass.fromElement(el, options); - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - } - } - catch(err) { - fabric.log(err); - } + if (attr === 'font') { + parseFontDeclaration(value, oStyle); } else { - checkIfDone(); + oStyle[attr] = value; } - } + }); } /** - * Returns CSS rules for a given SVG document - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} CSS rules of this document + * @private */ - function getCSSRules(doc) { - var styles = doc.getElementsByTagName('style'), - allRules = { }, - rules; + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') continue; - // very crude parsing of style contents - for (var i = 0, len = styles.length; i < len; i++) { - var styleContents = styles[0].textContent; + attr = normalizeAttr(prop.toLowerCase()); + value = normalizeValue(attr, style[prop]); - // remove comments - styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); - - rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); - rules = rules.map(function(rule) { return rule.trim(); }); - - rules.forEach(function(rule) { - var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); - rule = match[1]; - var declaration = match[2].trim(), - propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); - - if (!allRules[rule]) { - allRules[rule] = { }; - } - - for (var i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(/\s*:\s*/), - property = pair[0], - value = pair[1]; - - allRules[rule][property] = value; - } - }); + if (attr === 'font') { + parseFontDeclaration(value, oStyle); + } + else { + oStyle[attr] = value; + } } - - return allRules; } /** @@ -640,7 +434,7 @@ }; fabric.gradientDefs = fabric.getGradientDefs(doc); - fabric.cssRules = getCSSRules(doc); + fabric.cssRules = fabric.getCSSRules(doc); // Precedence of rules: style > class > attribute @@ -684,53 +478,6 @@ } }; - /** - * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) - * @memberof fabric - * @param {String} url - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromURL(url, callback, reviver) { - - url = url.replace(/^\n\s*/, '').trim(); - - svgCache.has(url, function (hasUrl) { - if (hasUrl) { - svgCache.get(url, function (value) { - var enlivedRecord = _enlivenCachedObject(value); - callback(enlivedRecord.objects, enlivedRecord.options); - }); - } - else { - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - } - }); - - function onComplete(r) { - - var xml = r.responseXML; - if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { - xml = new ActiveXObject('Microsoft.XMLDOM'); - xml.async = 'false'; - //IE chokes on DOCTYPE - xml.loadXML(r.responseText.replace(//i,'')); - } - if (!xml.documentElement) return; - - fabric.parseSVGDocument(xml.documentElement, function (results, options) { - svgCache.set(url, { - objects: fabric.util.array.invoke(results, 'toObject'), - options: options - }); - callback(results, options); - }, reviver); - } - } - /** * @private */ @@ -747,134 +494,361 @@ } /** - * Takes string corresponding to an SVG document, and parses it into a set of fabric objects - * @memberof fabric - * @param {String} string - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromString(string, callback, reviver) { - string = string.trim(); - var doc; - if (typeof DOMParser !== 'undefined') { - var parser = new DOMParser(); - if (parser && parser.parseFromString) { - doc = parser.parseFromString(string, 'text/xml'); - } - } - else if (fabric.window.ActiveXObject) { - doc = new ActiveXObject('Microsoft.XMLDOM'); - doc.async = 'false'; - //IE chokes on DOCTYPE - doc.loadXML(string.replace(//i,'')); - } - - fabric.parseSVGDocument(doc.documentElement, function (results, options) { - callback(results, options); - }, reviver); - } - - /** - * Creates markup containing SVG font faces - * @param {Array} objects Array of fabric objects - * @return {String} + * @private */ - function createSVGFontFacesMarkup(objects) { - var markup = ''; - - for (var i = 0, len = objects.length; i < len; i++) { - if (objects[i].type !== 'text' || !objects[i].path) continue; - - markup += [ - '@font-face {', - 'font-family: ', objects[i].fontFamily, '; ', - 'src: url(\'', objects[i].path, '\')', - '}' - ].join(''); - } - - if (markup) { - markup = [ - '' - ].join(''); - } - - return markup; - } - - /** - * Creates markup containing SVG referenced elements like patterns, gradients etc. - * @param {fabric.Canvas} canvas instance of fabric.Canvas - * @return {String} - */ - function createSVGRefElementsMarkup(canvas) { - var markup = ''; - - if (canvas.backgroundColor && canvas.backgroundColor.source) { - markup = [ - '', '' - ].join(''); + ); } - - return markup; - } - - /** - * Parses an SVG document, returning all of the gradient declarations found in it - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element - */ - function getGradientDefs(doc) { - var linearGradientEls = doc.getElementsByTagName('linearGradient'), - radialGradientEls = doc.getElementsByTagName('radialGradient'), - el, i, - gradientDefs = { }; - - i = linearGradientEls.length; - for (; i--; ) { - el = linearGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - i = radialGradientEls.length; - for (; i--; ) { - el = radialGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - return gradientDefs; } extend(fabric, { - parseAttributes: parseAttributes, - parseElements: parseElements, - parseStyleAttribute: parseStyleAttribute, - parsePointsAttribute: parsePointsAttribute, - getCSSRules: getCSSRules, + /** + * Initializes gradients on instances, according to gradients parsed from a document + * @param {Array} instances + */ + resolveGradients: function(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); - loadSVGFromURL: loadSVGFromURL, - loadSVGFromString: loadSVGFromString, + if (!(/^url\(/).test(instanceFillValue)) continue; - createSVGFontFacesMarkup: createSVGFontFacesMarkup, - createSVGRefElementsMarkup: createSVGRefElementsMarkup, + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - getGradientDefs: getGradientDefs + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, i, + gradientDefs = { }; + + i = linearGradientEls.length; + for (; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + i = radialGradientEls.length; + for (; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }; + + // if there's a parent container (`g` node), parse its attributes recursively upwards + if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { + attr = normalizeAttr(attr); + value = normalizeValue(attr, value, parentAttributes); + + memo[attr] = value; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + ownAttributes = extend(ownAttributes, + extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + + return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver) { + fabric.ElementsParser.parse(elements, callback, options, reviver); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) return oStyle; + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param points {String} points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ], i, len; + + // points could look like "10,20 30,40" or "10 20 30 40" + if (asPairs) { + i = 0; + len = points.length; + for (; i < len; i++) { + var pair = points[i].split(','); + parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); + } + } + else { + i = 0; + len = points.length; + for (; i < len; i+=2) { + parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, + rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); + rule = match[1]; + var declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberof fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + loadSVGFromURL: function(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i,'')); + } + if (!xml.documentElement) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberof fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + loadSVGFromString: function(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(//i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + }, + + /** + * Creates markup containing SVG font faces + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '' + ].join(''); + } + + return markup; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @param {fabric.Canvas} canvas instance of fabric.Canvas + * @return {String} + */ + createSVGRefElementsMarkup: function(canvas) { + var markup = [ ]; + + _createSVGPattern(markup, canvas, 'backgroundColor'); + _createSVGPattern(markup, canvas, 'overlayColor'); + + return markup.join(''); + } }); })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/pattern.class.js b/src/pattern.class.js index cf19a18d..c89d371a 100644 --- a/src/pattern.class.js +++ b/src/pattern.class.js @@ -3,6 +3,7 @@ * @class fabric.Pattern * @see {@link http://fabricjs.com/patterns/|Pattern demo} * @see {@link http://fabricjs.com/dynamic-patterns/|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition */ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { @@ -133,6 +134,11 @@ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ */ toLive: function(ctx) { var source = typeof this.source === 'function' ? this.source() : this.source; + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) return ''; + if (source.naturalWidth === 0 || source.naturalHeight === 0) return ''; + } return ctx.createPattern(source, this.repeat); } }); diff --git a/src/shadow.class.js b/src/shadow.class.js index e38edd54..46b797b5 100644 --- a/src/shadow.class.js +++ b/src/shadow.class.js @@ -13,6 +13,7 @@ * Shadow class * @class fabric.Shadow * @see {@link http://fabricjs.com/shadows/|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition */ fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { @@ -50,6 +51,13 @@ */ affectStroke: false, + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + /** * Constructor * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)") @@ -127,12 +135,28 @@ * @return {Object} Object representation of a shadow instance */ toObject: function() { - return { - color: this.color, - blur: this.blur, - offsetX: this.offsetX, - offsetY: this.offsetY - }; + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + if (this.color !== proto.color) { + obj.color = this.color; + } + if (this.blur !== proto.blur) { + obj.blur = this.blur; + } + if (this.offsetX !== proto.offsetX) { + obj.offsetX = this.offsetX; + } + if (this.offsetY !== proto.offsetY) { + obj.offsetY = this.offsetY; + } + return obj; } }); diff --git a/src/shapes/circle.class.js b/src/shapes/circle.class.js index 87c50d2f..b6390865 100644 --- a/src/shapes/circle.class.js +++ b/src/shapes/circle.class.js @@ -15,6 +15,7 @@ * Circle class * @class fabric.Circle * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition */ fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { diff --git a/src/shapes/ellipse.class.js b/src/shapes/ellipse.class.js index e970a407..fd58efac 100644 --- a/src/shapes/ellipse.class.js +++ b/src/shapes/ellipse.class.js @@ -16,6 +16,7 @@ * @class fabric.Ellipse * @extends fabric.Object * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition */ fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index c324a833..0c21a0c6 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -31,6 +31,7 @@ * @extends fabric.Object * @mixes fabric.Collection * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#groups} + * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { @@ -76,26 +77,28 @@ * @private */ _updateObjectsCoords: function() { - var groupDeltaX = this.left, - groupDeltaY = this.top; + this.forEachObject(this._updateObjectCoords, this); + }, - this.forEachObject(function(object) { + /** + * @private + */ + _updateObjectCoords: function(object) { + var objectLeft = object.getLeft(), + objectTop = object.getTop(); - var objectLeft = object.get('left'), - objectTop = object.get('top'); + object.set({ + originalLeft: objectLeft, + originalTop: objectTop, + left: objectLeft - this.left, + top: objectTop - this.top + }); - object.set('originalLeft', objectLeft); - object.set('originalTop', objectTop); + object.setCoords(); - object.set('left', objectLeft - groupDeltaX); - object.set('top', objectTop - groupDeltaY); - - object.setCoords(); - - // do not display corners of objects enclosed in a group - object.__origHasControls = object.hasControls; - object.hasControls = false; - }, this); + // do not display corners of objects enclosed in a group + object.__origHasControls = object.hasControls; + object.hasControls = false; }, /** @@ -106,14 +109,6 @@ return '#'; }, - /** - * Returns an array of all objects in this group - * @return {Array} group objects - */ - getObjects: function() { - return this._objects; - }, - /** * Adds an object to a group; Then recalculates group's dimension, position. * @param {Object} object @@ -125,12 +120,20 @@ this._objects.push(object); object.group = this; // since _restoreObjectsState set objects inactive - this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this); + this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); return this; }, + /** + * @private + */ + _setObjectActive: function(object) { + object.set('active', true); + object.group = this; + }, + /** * Removes an object from a group; Then recalculates group's dimension, position. * @param {Object} object @@ -140,12 +143,14 @@ removeWithUpdate: function(object) { this._moveFlippedObject(object); this._restoreObjectsState(); + // since _restoreObjectsState set objects inactive - this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this); + this.forEachObject(this._setObjectActive, this); this.remove(object); this._calcBounds(); this._updateObjectsCoords(); + return this; }, @@ -217,38 +222,47 @@ // do not render if object is not visible if (!this.visible) return; - var v = this.canvas.viewportTransform; ctx.save(); - var sxy = fabric.util.transformPoint( - new fabric.Point(this.scaleX, this.scaleY), - v, - true - ), - groupScaleFactor = Math.max(sxy.x, sxy.y); - this.clipTo && fabric.util.clipContext(this, ctx); - //The array is now sorted in order of highest first, so start from end. + // the array is now sorted in order of highest first, so start from end for (var i = 0, len = this._objects.length; i < len; i++) { - - var object = this._objects[i], - originalScaleFactor = object.borderScaleFactor, - originalHasRotatingPoint = object.hasRotatingPoint; - - // do not render if object is not visible - if (!object.visible) continue; - - object.hasRotatingPoint = false; - object.render(ctx); - - object.hasRotatingPoint = originalHasRotatingPoint; + this._renderObject(this._objects[i], ctx); } + this.clipTo && ctx.restore(); this.callSuper('_renderControls', ctx, noTransform); ctx.restore(); }, + /** + * @private + */ + _renderObject: function(object, ctx) { + var v = this.canvas.viewportTransform, + sxy = fabric.util.transformPoint( + new fabric.Point(this.scaleX, this.scaleY), + v, + true + ); + + var originalScaleFactor = object.borderScaleFactor, + originalHasRotatingPoint = object.hasRotatingPoint, + groupScaleFactor = Math.max(sxy.x, sxy.y); + + // do not render if object is not visible + if (!object.visible) return; + + object.borderScaleFactor = groupScaleFactor; + object.hasRotatingPoint = false; + + object.render(ctx); + + object.borderScaleFactor = originalScaleFactor; + object.hasRotatingPoint = originalHasRotatingPoint; + }, + /** * Retores original state of each of group objects (original state is that which was before group was created). * @private @@ -267,9 +281,10 @@ * @return {fabric.Group} thisArg */ _moveFlippedObject: function(object) { - var oldOriginX = object.get('originX'); - var oldOriginY = object.get('originY'); - var center = object.getCenterPoint(); + var oldOriginX = object.get('originX'), + oldOriginY = object.get('originY'), + center = object.getCenterPoint(); + object.set({ originX: 'center', originY: 'center', @@ -277,6 +292,24 @@ top: center.y }); + this._toggleFlipping(object); + + var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); + + object.set({ + originX: oldOriginX, + originY: oldOriginY, + left: newOrigin.x, + top: newOrigin.y + }); + + return this; + }, + + /** + * @private + */ + _toggleFlipping: function(object) { if (this.flipX) { object.toggle('flipX'); object.set('left', -object.get('left')); @@ -287,15 +320,6 @@ object.set('top', -object.get('top')); object.setAngle(-object.getAngle()); } - - var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); - object.set({ - originX: oldOriginX, - originY: oldOriginY, - left: newOrigin.x, - top: newOrigin.y - }); - return this; }, /** @@ -305,19 +329,7 @@ * @return {fabric.Group} thisArg */ _restoreObjectState: function(object) { - var groupLeft = this.get('left'), - groupTop = this.get('top'), - groupAngle = this.getAngle() * (Math.PI / 180), - rotatedTop = Math.cos(groupAngle) * object.get('top') * this.get('scaleY') + Math.sin(groupAngle) * object.get('left') * this.get('scaleX'), - rotatedLeft = -Math.sin(groupAngle) * object.get('top') * this.get('scaleY') + Math.cos(groupAngle) * object.get('left') * this.get('scaleX'); - - object.setAngle(object.getAngle() + this.getAngle()); - - object.set('left', groupLeft + rotatedLeft); - object.set('top', groupTop + rotatedTop); - - object.set('scaleX', object.get('scaleX') * this.get('scaleX')); - object.set('scaleY', object.get('scaleY') * this.get('scaleY')); + this._setObjectPosition(object); object.setCoords(); object.hasControls = object.__origHasControls; @@ -329,6 +341,37 @@ return this; }, + /** + * @private + */ + _setObjectPosition: function(object) { + var groupLeft = this.getLeft(), + groupTop = this.getTop(), + rotated = this._getRotatedLeftTop(object); + + object.set({ + angle: object.getAngle() + this.getAngle(), + left: groupLeft + rotated.left, + top: groupTop + rotated.top, + scaleX: object.get('scaleX') * this.get('scaleX'), + scaleY: object.get('scaleY') * this.get('scaleY') + }); + }, + + /** + * @private + */ + _getRotatedLeftTop: function(object) { + var groupAngle = this.getAngle() * (Math.PI / 180); + return { + left: (-Math.sin(groupAngle) * object.getTop() * this.get('scaleY') + + Math.cos(groupAngle) * object.getLeft() * this.get('scaleX')), + + top: (Math.cos(groupAngle) * object.getTop() * this.get('scaleY') + + Math.sin(groupAngle) * object.getLeft() * this.get('scaleX')) + }; + }, + /** * Destroys a group (restoring state of its objects) * @return {fabric.Group} thisArg @@ -394,11 +437,9 @@ _calcBounds: function() { var aX = [], aY = [], - minX, minY, maxX, maxY, o, width, height, minXY, maxXY, - i = 0, - len = this._objects.length; + o; - for (; i < len; ++i) { + for (var i = 0, len = this._objects.length; i < len; ++i) { o = this._objects[i]; o.setCoords(); for (var prop in o.oCoords) { @@ -406,7 +447,14 @@ aY.push(o.oCoords[prop].y); } } - + + this.set(this._getBounds(aX, aY)); + }, + + /** + * @private + */ + _getBounds: function(aX, aY) { var ivt; if (this.canvas) { ivt = fabric.util.invertTransform(this.canvas.viewportTransform); @@ -415,18 +463,15 @@ ivt = [1, 0, 0, 1, 0, 0]; console.log('no canvas'); } + var minXY = fabric.util.transformPoint(new fabric.Point(min(aX), min(aY)), ivt), + maxXY = fabric.util.transformPoint(new fabric.Point(max(aX), max(aY)), ivt); - minXY = new fabric.Point(min(aX), min(aY)); - maxXY = new fabric.Point(max(aX), max(aY)); - - minXY = fabric.util.transformPoint(minXY, ivt); - maxXY = fabric.util.transformPoint(maxXY, ivt); - - this.width = (maxXY.x - minXY.x) || 0; - this.height = (maxXY.y - minXY.y) || 0; - - this.left = (minXY.x + maxXY.x) / 2 || 0; - this.top = (minXY.y + maxXY.y) / 2 || 0; + return { + width: (maxXY.x - minXY.x) || 0, + height: (maxXY.y - minXY.y) || 0, + left: (minXY.x + maxXY.x) / 2 || 0, + top: (minXY.y + maxXY.y) / 2 || 0, + }; }, /* _TO_SVG_START_ */ diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 8a25ccb5..66cecfe4 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -18,6 +18,7 @@ * @class fabric.Image * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#images} + * @see {@link fabric.Image#initialize} for constructor definition */ fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { @@ -28,6 +29,14 @@ */ type: 'image', + /** + * crossOrigin value (one of "", "anonymous", "allow-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + /** * Constructor * @param {HTMLImageElement | String} element Image element @@ -40,7 +49,8 @@ this.filters = [ ]; this.callSuper('initialize', options); - this._initElement(element); + + this._initElement(element, options); this._initConfig(options); if (options.filters) { @@ -78,6 +88,18 @@ return this; }, + /** + * Sets crossOrigin value (on an instance and corresponding image element) + * @return {fabric.Image} thisArg + * @chainable + */ + setCrossOrigin: function(value) { + this.crossOrigin = value; + this._element.crossOrigin = value; + + return this; + }, + /** * Returns original size of an image * @return {Object} Object with "width" and "height" properties @@ -108,7 +130,7 @@ else { v = [1, 0, 0, 1, 0, 0]; // TODO: this isn't a solution } - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); @@ -144,14 +166,7 @@ */ _stroke: function(ctx) { ctx.save(); - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - + this._setStrokeStyles(ctx); ctx.beginPath(); ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); ctx.closePath(); @@ -169,13 +184,7 @@ h = this.height; ctx.save(); - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; + this._setStrokeStyles(ctx); ctx.beginPath(); fabric.util.drawDashedLine(ctx, x, y, x+w, y, this.strokeDashArray); @@ -196,7 +205,8 @@ src: this._originalElement.src || this._originalElement._src, filters: this.filters.map(function(filterObj) { return filterObj && filterObj.toObject(); - }) + }), + crossOrigin: this.crossOrigin }); }, @@ -219,7 +229,8 @@ '" transform="translate(' + (-this.width/2) + ' ' + (-this.height/2) + ')', '" width="', this.width, '" height="', this.height, - '">' + '" preserveAspectRatio="none"', + '>' ); if (this.stroke || this.strokeDashArray) { @@ -362,6 +373,7 @@ options || (options = { }); this.setOptions(options); this._setWidthHeight(options); + this._element.crossOrigin = this.crossOrigin; }, /** @@ -424,28 +436,13 @@ * @param {Function} [callback] Callback to invoke when an image instance is created */ fabric.Image.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.src; - - /** @ignore */ - img.onload = function() { + fabric.util.loadImage(object.src, function(img) { fabric.Image.prototype._initFilters.call(object, object, function(filters) { object.filters = filters || [ ]; - var instance = new fabric.Image(img, object); callback && callback(instance); - img = img.onload = img.onerror = null; }); - }; - - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback(null, true); - img = img.onload = img.onerror = null; - }; - - img.src = src; + }, null, object.crossOrigin); }; /** @@ -458,7 +455,7 @@ fabric.Image.fromURL = function(url, callback, imgOptions) { fabric.util.loadImage(url, function(img) { callback(new fabric.Image(img, imgOptions)); - }); + }, null, imgOptions && imgOptions.crossOrigin); }; /* _FROM_SVG_START_ */ diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js new file mode 100644 index 00000000..c493edd0 --- /dev/null +++ b/src/shapes/itext.class.js @@ -0,0 +1,989 @@ +(function() { + + var clone = fabric.util.object.clone; + + /** + * IText class (introduced in v1.4) + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires text:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

Supported key combinations:

+ *
+    *   Move cursor:                    left, right, up, down
+    *   Select character:               shift + left, shift + right
+    *   Select text vertically:         shift + up, shift + down
+    *   Move cursor by word:            alt + left, alt + right
+    *   Select words:                   shift + alt + left, shift + alt + right
+    *   Move cursor to line start/end:  cmd + left, cmd + right
+    *   Select till start/end of line:  cmd + shift + left, cmd + shift + right
+    *   Jump to start/end of text:      cmd + up, cmd + down
+    *   Select till start/end of text:  cmd + shift + up, cmd + shift + down
+    *   Delete character:               backspace
+    *   Delete word:                    alt + backspace
+    *   Delete line:                    cmd + backspace
+    *   Forward delete:                 delete
+    *   Copy text:                      ctrl/cmd + c
+    *   Paste text:                     ctrl/cmd + v
+    *   Cut text:                       ctrl/cmd + x
+    * 
+ * + *

Supported mouse/touch combination

+ *
+    *   Position cursor:                click/touch
+    *   Create selection:               click/touch & drag
+    *   Create selection:               click & shift + click
+    *   Select word:                    double click
+    *   Select line:                    triple click
+    * 
+ */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', + + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Nubmer + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Nubmer + * @default + */ + selectionEnd: 0, + + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', + + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, + + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, + + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of default cursor (when not overwritten by character style) + * @type String + * @default + */ + cursorColor: '#333', + + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Object containing character styles + * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) + * @type Object + * @default + */ + styles: null, + + skipFillStrokeCheck: true, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _fontSizeFraction: 4, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + _charWidthsCache: { }, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.callSuper('initialize', text, options); + this.initBehavior(); + + fabric.IText.instances.push(this); + + // caching + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * Returns true if object has no styling + */ + isEmptyStyles: function() { + if (!this.styles) return true; + var obj = this.styles; + + for (var p1 in obj) { + for (var p2 in obj[p1]) { + /*jshint unused:false */ + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + this.selectionStart = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + this.selectionEnd = index; + this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index); + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * @return {Object} styles Style object at a cursor position + */ + getSelectionStyles: function() { + var loc = this.get2DCursorLocation(); + if (this.styles[loc.lineIndex]) { + return this.styles[loc.lineIndex][loc.charIndex] || { }; + } + return { }; + }, + + /** + * Sets style of a current selection + * @param {Object} [styles] Styles object + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles) { + if (this.selectionStart === this.selectionEnd) { + this._extendStyles(this.selectionStart, styles); + } + else { + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + this._extendStyles(i, styles); + } + } + return this; + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this.styles[loc.lineIndex]) { + this.styles[loc.lineIndex] = { }; + } + if (!this.styles[loc.lineIndex][loc.charIndex]) { + this.styles[loc.lineIndex][loc.charIndex] = { }; + } + + fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + this.ctx = ctx; + this.isEditing && this.renderCursorOrSelection(); + }, + + /** + * Renders cursor or selection (depending on what exists) + */ + renderCursorOrSelection: function() { + if (!this.active) return; + + var chars = this.text.split(''), + boundaries; + + if (this.selectionStart === this.selectionEnd) { + boundaries = this._getCursorBoundaries(chars, 'cursor'); + this.renderCursor(boundaries); + } + else { + boundaries = this._getCursorBoundaries(chars, 'selection'); + this.renderSelection(chars, boundaries); + } + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + */ + get2DCursorLocation: function(selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var textBeforeCursor = this.text.slice(0, selectionStart); + var linesBeforeCursor = textBeforeCursor.split(this._reNewline); + + return { + lineIndex: linesBeforeCursor.length - 1, + charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length + }; + }, + + /** + * Returns fontSize of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {Number} Character font size + */ + getCurrentCharFontSize: function(lineIndex, charIndex) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fontSize) || this.fontSize; + }, + + /** + * Returns color (fill) of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {String} Character color (fill) + */ + getCurrentCharColor: function(lineIndex, charIndex) { + return ( + this.styles[lineIndex] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && + this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor; + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(chars, typeOfBoundaries) { + + var cursorLocation = this.get2DCursorLocation(), + + textLines = this.text.split(this._reNewline), + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + left = Math.round(this._getLeftOffset()), + top = -this.height / 2, + + offsets = this._getCursorBoundariesOffsets( + chars, typeOfBoundaries, cursorLocation, textLines); + + return { + left: left, + top: top, + leftOffset: offsets.left + offsets.lineLeft, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries, cursorLocation, textLines) { + + var lineLeftOffset = 0, + + lineIndex = 0, + charIndex = 0, + + leftOffset = 0, + topOffset = typeOfBoundaries === 'cursor' + // selection starts at the very top of the line, + // whereas cursor starts at the padding created by line height + ? (this._getHeightOfLine(this.ctx, 0) - + this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)) + : 0; + + for (var i = 0; i < this.selectionStart; i++) { + if (chars[i] === '\n') { + leftOffset = 0; + var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0); + topOffset += this._getCachedLineHeight(index); + + lineIndex++; + charIndex = 0; + } + else { + leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); + charIndex++; + } + + lineLeftOffset = this._getCachedLineOffset(lineIndex, textLines); + } + + this._clearCache(); + + return { + top: topOffset, + left: leftOffset, + lineLeft: lineLeftOffset + }; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = { }; + this.__lineHeights = { }; + this.__lineOffsets = { }; + }, + + /** + * @private + */ + _getCachedLineHeight: function(index) { + return this.__lineHeights[index] || + (this.__lineHeights[index] = this._getHeightOfLine(this.ctx, index)); + }, + + /** + * @private + */ + _getCachedLineWidth: function(lineIndex, textLines) { + return this.__lineWidths[lineIndex] || + (this.__lineWidths[lineIndex] = this._getWidthOfLine(this.ctx, lineIndex, textLines)); + }, + + /** + * @private + */ + _getCachedLineOffset: function(lineIndex, textLines) { + var widthOfLine = this._getCachedLineWidth(lineIndex, textLines); + + return this.__lineOffsets[lineIndex] || + (this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine)); + }, + + /** + * Renders cursor + * @param {Object} boundaries + */ + renderCursor: function(boundaries) { + var ctx = this.ctx; + + ctx.save(); + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getCurrentCharFontSize(lineIndex, charIndex); + + ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); + ctx.globalAlpha = this._currentCursorOpacity; + + ctx.fillRect( + boundaries.left + boundaries.leftOffset, + boundaries.top + boundaries.topOffset, + this.cursorWidth / this.scaleX, + charHeight); + + ctx.restore(); + }, + + /** + * Renders text selection + * @param {Array} chars Array of characters + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + */ + renderSelection: function(chars, boundaries) { + var ctx = this.ctx; + + ctx.save(); + + ctx.fillStyle = this.selectionColor; + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + textLines = this.text.split(this._reNewline), + origLineIndex = lineIndex; + + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + + if (chars[i] === '\n') { + boundaries.leftOffset = 0; + boundaries.topOffset += this._getHeightOfLine(ctx, lineIndex); + lineIndex++; + charIndex = 0; + } + else if (i !== this.text.length) { + + var charWidth = this._getWidthOfChar(ctx, chars[i], lineIndex, charIndex), + lineOffset = this._getLineLeftOffset(this._getWidthOfLine(ctx, lineIndex, textLines)) || 0; + + if (lineIndex === origLineIndex) { + // only offset the line if we're rendering selection of 2nd, 3rd, etc. line + lineOffset = 0; + } + + ctx.fillRect( + boundaries.left + boundaries.leftOffset + lineOffset, + boundaries.top + boundaries.topOffset, + charWidth, + this._getHeightOfLine(ctx, lineIndex)); + + boundaries.leftOffset += charWidth; + charIndex++; + } + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + + if (this.isEmptyStyles()) { + return this._renderCharsFast(method, ctx, line, left, top); + } + + this.skipTextAlign = true; + + // set proper box offset + left -= this.textAlign === 'center' + ? (this.width / 2) + : (this.textAlign === 'right') + ? this.width + : 0; + + // set proper line offset + var textLines = this.text.split(this._reNewline), + lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines), + lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth), + chars = line.split(''); + + left += lineLeftOffset || 0; + + ctx.save(); + for (var i = 0, len = chars.length; i < len; i++) { + this._renderChar(method, ctx, lineIndex, i, chars[i], left, top, lineHeight); + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderCharsFast: function(method, ctx, line, left, top) { + this.skipTextAlign = false; + + if (method === 'fillText' && this.fill) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + if (method === 'strokeText' && this.stroke) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { + var decl, charWidth; + + if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { + + var shouldStroke = decl.stroke || this.stroke, + shouldFill = decl.fill || this.fill; + + ctx.save(); + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl); + + if (shouldFill) { + ctx.fillText(_char, left, top); + } + if (shouldStroke) { + ctx.strokeText(_char, left, top); + } + + this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight); + ctx.restore(); + + ctx.translate(charWidth, 0); + } + else { + if (method === 'strokeText' && this.stroke) { + ctx[method](_char, left, top); + } + if (method === 'fillText' && this.fill) { + ctx[method](_char, left, top); + } + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i); + this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight); + + ctx.translate(ctx.measureText(_char).width, 0); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight) { + var textDecoration = styleDeclaration + ? (styleDeclaration.textDecoration || this.textDecoration) + : this.textDecoration; + + if (!textDecoration) return; + + if (textDecoration.indexOf('underline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + 0 + ); + } + if (textDecoration.indexOf('line-through') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top + (this.fontSize / this._fontSizeFraction), + charWidth, + (lineHeight / this._fontSizeFraction) + ); + } + if (textDecoration.indexOf('overline') > -1) { + this._renderCharDecorationAtOffset( + ctx, + left, + top, + charWidth, + lineHeight - (this.fontSize / this._fontSizeFraction) + ); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset) { + ctx.fillRect(left, top - offset, charWidth, 1); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine + top += this.fontSize / 4; + this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines + */ + _renderTextDecoration: function(ctx, textLines) { + if (this.isEmptyStyles()) { + return this.callSuper('_renderTextDecoration', ctx, textLines); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} textLines Array of all text lines + */ + _renderTextLinesBackground: function(ctx, textLines) { + if (!this.textBackgroundColor && !this.styles) return; + + ctx.save(); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + } + + var lineHeights = 0, + fractionOfFontSize = this.fontSize / this._fontSizeFraction; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var heightOfLine = this._getHeightOfLine(ctx, i, textLines); + if (textLines[i] === '') { + lineHeights += heightOfLine; + continue; + } + + var lineWidth = this._getWidthOfLine(ctx, i, textLines), + lineLeftOffset = this._getLineLeftOffset(lineWidth); + + if (this.textBackgroundColor) { + ctx.fillStyle = this.textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset, + this._getTopOffset() + lineHeights + fractionOfFontSize, + lineWidth, + heightOfLine + ); + } + if (this.styles[i]) { + for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { + + var _char = textLines[i][j]; + + ctx.fillStyle = this.styles[i][j].textBackgroundColor; + + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines), + this._getTopOffset() + lineHeights + fractionOfFontSize, + this._getWidthOfChar(ctx, _char, i, j, textLines) + 1, + heightOfLine + ); + } + } + } + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + */ + _getCacheProp: function(_char, styleDeclaration) { + return _char + + + styleDeclaration.fontFamily + + styleDeclaration.fontSize + + styleDeclaration.fontWeight + + styleDeclaration.fontStyle + + + styleDeclaration.shadow; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} _char + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} [decl] + */ + _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { + var styleDeclaration = decl || (this.styles[lineIndex] && this.styles[lineIndex][charIndex]); + + if (styleDeclaration) { + // cloning so that original style object is not polluted with following font declarations + styleDeclaration = clone(styleDeclaration); + } + else { + styleDeclaration = { }; + } + + this._applyFontStyles(styleDeclaration); + + var cacheProp = this._getCacheProp(_char, styleDeclaration); + + // short-circuit if no styles + if (this.isEmptyStyles() && this._charWidthsCache[cacheProp]) { + return this._charWidthsCache[cacheProp]; + } + + if (typeof styleDeclaration.shadow === 'string') { + styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); + } + + var fill = styleDeclaration.fill || this.fill; + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx) + : fill; + + if (styleDeclaration.stroke) { + ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) + ? styleDeclaration.stroke.toLive(ctx) + : styleDeclaration.stroke; + } + + ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; + ctx.font = this._getFontDeclaration.call(styleDeclaration); + this._setShadow.call(styleDeclaration, ctx); + + if (!this._charWidthsCache[cacheProp]) { + this._charWidthsCache[cacheProp] = ctx.measureText(_char).width; + } + return this._charWidthsCache[cacheProp]; + }, + + /** + * @private + * @param {Object} styleDeclaration + */ + _applyFontStyles: function(styleDeclaration) { + if (!styleDeclaration.fontFamily) { + styleDeclaration.fontFamily = this.fontFamily; + } + if (!styleDeclaration.fontSize) { + styleDeclaration.fontSize = this.fontSize; + } + if (!styleDeclaration.fontWeight) { + styleDeclaration.fontWeight = this.fontWeight; + } + if (!styleDeclaration.fontStyle) { + styleDeclaration.fontStyle = this.fontStyle; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { + ctx.save(); + var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); + ctx.restore(); + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfChar: function(ctx, _char, lineIndex, charIndex) { + if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) { + return this.styles[lineIndex][charIndex].fontSize || this.fontSize; + } + return this.fontSize; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getWidthOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) { + lines = lines || this.text.split(this._reNewline); + var _char = lines[lineIndex].split('')[charIndex]; + return this._getHeightOfChar(ctx, _char, lineIndex, charIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) { + var width = 0; + for (var i = 0; i < charIndex; i++) { + width += this._getWidthOfCharAt(ctx, lineIndex, i, lines); + } + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfLine: function(ctx, lineIndex, textLines) { + // if (!this.styles[lineIndex]) { + // return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]); + // } + return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextWidth: function(ctx, textLines) { + + if (this.isEmptyStyles()) { + return this.callSuper('_getTextWidth', ctx, textLines); + } + + var maxWidth = this._getWidthOfLine(ctx, 0, textLines); + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = this._getWidthOfLine(ctx, i, textLines); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfLine: function(ctx, lineIndex, textLines) { + + textLines = textLines || this.text.split(this._reNewline); + + var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0); + + var line = textLines[lineIndex]; + var chars = line.split(''); + + for (var i = 1, len = chars.length; i < len; i++) { + var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i); + if (currentCharHeight > maxHeight) { + maxHeight = currentCharHeight; + } + } + + return maxHeight * this.lineHeight; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextHeight: function(ctx, textLines) { + var height = 0; + for (var i = 0, len = textLines.length; i < len; i++) { + height += this._getHeightOfLine(ctx, i, textLines); + } + return height; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTopOffset: function() { + var topOffset = fabric.Text.prototype._getTopOffset.call(this); + return topOffset - (this.fontSize / this._fontSizeFraction); + }, + + /** + * Returns object representation of an instance + * @methd toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { + styles: clone(this.styles) + }); + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @return {fabric.IText} instance of fabric.IText + */ + fabric.IText.fromObject = function(object) { + return new fabric.IText(object.text, clone(object)); + }; + + fabric.IText.instances = [ ]; + +})(); diff --git a/src/shapes/line.class.js b/src/shapes/line.class.js index 2556f4e9..321ec458 100644 --- a/src/shapes/line.class.js +++ b/src/shapes/line.class.js @@ -16,6 +16,7 @@ * Line class * @class fabric.Line * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition */ fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { @@ -83,7 +84,7 @@ _render: function(ctx) { ctx.beginPath(); - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); } diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 4275bf36..7ec90fc8 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -17,6 +17,7 @@ * Root object class from which all 2d shape classes inherit from * @class fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#objects} + * @see {@link fabric.Object#initialize} for constructor definition * * @fires added * @fires removed @@ -307,14 +308,14 @@ * @type String * @default */ - originX: 'center', + originX: 'left', /** * Vertical origin of transformation of an object (one of "top", "bottom", "center") * @type String * @default */ - originY: 'center', + originY: 'top', /** * Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom} @@ -446,7 +447,7 @@ * @type Boolean * @default */ - centeredRotation: false, + centeredRotation: true, /** * Color of object's fill @@ -799,95 +800,6 @@ return this.toObject(propertiesToInclude); }, - /* _TO_SVG_START_ */ - /** - * Returns styles-string for svg-export - * @return {String} - */ - getSvgStyles: function() { - - var fill = this.fill - ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) - : 'none'; - - var stroke = this.stroke - ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) - : 'none'; - - var strokeWidth = this.strokeWidth ? this.strokeWidth : '0'; - var strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : ''; - var strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt'; - var strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter'; - var strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4'; - var opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1'; - - var visibility = this.visible ? '' : " visibility: hidden;"; - var filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; - - return [ - "stroke: ", stroke, "; ", - "stroke-width: ", strokeWidth, "; ", - "stroke-dasharray: ", strokeDashArray, "; ", - "stroke-linecap: ", strokeLineCap, "; ", - "stroke-linejoin: ", strokeLineJoin, "; ", - "stroke-miterlimit: ", strokeMiterLimit, "; ", - "fill: ", fill, "; ", - "opacity: ", opacity, ";", - filter, - visibility - ].join(''); - }, - - /** - * Returns transform-string for svg-export - * @return {String} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - var center = this.getCenterPoint(); - - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var translatePart = "translate(" + - toFixed(center.x, NUM_FRACTION_DIGITS) + - " " + - toFixed(center.y, NUM_FRACTION_DIGITS) + - ")"; - - var anglePart = angle !== 0 - ? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")") - : ''; - - var scalePart = (this.scaleX === 1 && this.scaleY === 1) - ? '' : - (" scale(" + - toFixed(this.scaleX, NUM_FRACTION_DIGITS) + - " " + - toFixed(this.scaleY, NUM_FRACTION_DIGITS) + - ")"); - - var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : ""; - var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : ""; - - return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); - }, - - _createBaseSVGMarkup: function() { - var markup = [ ]; - - if (this.fill && this.fill.toLive) { - markup.push(this.fill.toSVG(this, false)); - } - if (this.stroke && this.stroke.toLive) { - markup.push(this.stroke.toSVG(this, false)); - } - if (this.shadow) { - markup.push(this.shadow.toSVG(this)); - } - return markup; - }, - /* _TO_SVG_END_ */ - /** * @private * @param {Object} object @@ -1014,6 +926,27 @@ ctx.save(); + this._transform(ctx, noTransform); + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + + var m = this.transformMatrix; + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._setShadow(ctx); + this.clipTo && fabric.util.clipContext(this, ctx); + this._render(ctx, noTransform); + this.clipTo && ctx.restore(); + this._removeShadow(ctx); + ctx.restore(); + + this._renderControls(ctx, noTransform); + }, + + _transform: function(ctx, noTransform) { var m = this.transformMatrix; var v; if (this.canvas) { @@ -1028,11 +961,12 @@ if (m && !this.group) { ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); } - if (!noTransform) { this.transform(ctx); } + }, + _setStrokeStyles: function(ctx) { if (this.stroke) { ctx.lineWidth = this.strokeWidth; ctx.lineCap = this.strokeLineCap; @@ -1042,26 +976,14 @@ ? this.stroke.toLive(ctx) : this.stroke; } + }, + _setFillStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive ? this.fill.toLive(ctx) : this.fill; } - - if (m && this.group) { - ctx.translate(-this.group.width/2, -this.group.height/2); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - this._setShadow(ctx); - this.clipTo && fabric.util.clipContext(this, ctx); - this._render(ctx, noTransform); - this.clipTo && ctx.restore(); - this._removeShadow(ctx); - ctx.restore(); - - this._renderControls(ctx, noTransform); }, /** @@ -1242,10 +1164,12 @@ this.set('active', false); this.setPositionByOrigin(new fabric.Point(el.width / 2, el.height / 2), 'center', 'center'); + var originalCanvas = this.canvas; canvas.add(this); var data = canvas.toDataURL(options); this.set(origParams).setCoords(); + this.canvas = originalCanvas; canvas.dispose(); canvas = null; @@ -1442,7 +1366,8 @@ * @chainable */ center: function () { - return this.centerH().centerV(); + this.canvas.centerObject(this); + return this; }, /** @@ -1455,81 +1380,18 @@ }, /** - * Moves an object to the bottom of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) */ - sendToBack: function() { - if (this.group) { - fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); - } - else { - this.canvas.sendToBack(this); - } - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - if (this.group) { - fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); - } - else { - this.canvas.bringToFront(this); - } - return this; - }, - - /** - * Moves an object down in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function(intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); - } - else { - this.canvas.sendBackwards(this, intersecting); - } - return this; - }, - - /** - * Moves an object up in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function(intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); - } - else { - this.canvas.bringForward(this, intersecting); - } - return this; - }, - - /** - * Moves an object to specified level in stack of drawn objects - * @param {Number} index New position of object - * @return {fabric.Object} thisArg - * @chainable - */ - moveTo: function(index) { - if (this.group) { - fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); - } - else { - this.canvas.moveTo(this, index); - } - return this; + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + return { + x: pointer.x - objectLeftTop.x, + y: pointer.y - objectLeftTop.y + }; } }); diff --git a/src/shapes/path.class.js b/src/shapes/path.class.js index 6fd33c85..b65f8ae2 100644 --- a/src/shapes/path.class.js +++ b/src/shapes/path.class.js @@ -51,6 +51,7 @@ * @class fabric.Path * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition */ fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { @@ -154,7 +155,8 @@ tempControlX, tempControlY, l = -((this.width / 2) + this.pathOffset.x), - t = -((this.height / 2) + this.pathOffset.y); + t = -((this.height / 2) + this.pathOffset.y), + methodName; for (var i = 0, len = this.path.length; i < len; ++i) { @@ -198,14 +200,20 @@ x += current[1]; y += current[2]; // draw a line if previous command was moveTo as well (otherwise, it will have no effect) - ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t); + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; // draw a line if previous command was moveTo as well (otherwise, it will have no effect) - ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t); + methodName = (previous && (previous[0] === 'm' || previous[0] === 'M')) + ? 'lineTo' + : 'moveTo'; + ctx[methodName](x + l, y + t); break; case 'c': // bezierCurveTo, relative @@ -259,7 +267,9 @@ tempY + t ); // set control point to 2nd one of this command - // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." controlX = x + current[1]; controlY = y + current[2]; @@ -285,7 +295,9 @@ y = tempY; // set control point to 2nd one of this command - // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." controlX = current[1]; controlY = current[2]; @@ -447,24 +459,8 @@ if (!noTransform) { this.transform(ctx); } - // ctx.globalCompositeOperation = this.fillRule; - - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx) - : this.fill; - } - - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - } - + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); ctx.beginPath(); @@ -612,49 +608,10 @@ _parseDimensions: function() { var aX = [], aY = [], - previousX, - previousY, - isLowerCase = false, - x, - y; + previous = { }; this.path.forEach(function(item, i) { - if (item[0] !== 'H') { - previousX = (i === 0) ? getX(item) : getX(this.path[i-1]); - } - if (item[0] !== 'V') { - previousY = (i === 0) ? getY(item) : getY(this.path[i-1]); - } - - // lowercased letter denotes relative position; - // transform to absolute - if (item[0] === item[0].toLowerCase()) { - isLowerCase = true; - } - - // last 2 items in an array of coordinates are the actualy x/y (except H/V); - // collect them - - // TODO (kangax): support relative h/v commands - - x = isLowerCase - ? previousX + getX(item) - : item[0] === 'V' - ? previousX - : getX(item); - - y = isLowerCase - ? previousY + getY(item) - : item[0] === 'H' - ? previousY - : getY(item); - - var val = parseInt(x, 10); - if (!isNaN(val)) aX.push(val); - - val = parseInt(y, 10); - if (!isNaN(val)) aY.push(val); - + this._getCoordsFromCommand(item, i, aX, aY, previous); }, this); var minX = min(aX), @@ -672,6 +629,51 @@ }; return o; + }, + + _getCoordsFromCommand: function(item, i, aX, aY, previous) { + var isLowerCase = false; + + if (item[0] !== 'H') { + previous.x = (i === 0) ? getX(item) : getX(this.path[i - 1]); + } + if (item[0] !== 'V') { + previous.y = (i === 0) ? getY(item) : getY(this.path[i - 1]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + var xy = this._getXY(item, isLowerCase, previous); + + var val = parseInt(xy.x, 10); + if (!isNaN(val)) aX.push(val); + + val = parseInt(xy.y, 10); + if (!isNaN(val)) aY.push(val); + }, + + _getXY: function(item, isLowerCase, previous) { + + // last 2 items in an array of coordinates are the actualy x/y (except H/V), collect them + // TODO (kangax): support relative h/v commands + + var x = isLowerCase + ? previous.x + getX(item) + : item[0] === 'V' + ? previous.x + : getX(item); + + var y = isLowerCase + ? previous.y + getY(item) + : item[0] === 'H' + ? previous.y + : getY(item); + + return { x: x, y: y }; } }); diff --git a/src/shapes/path_group.class.js b/src/shapes/path_group.class.js index 54431d06..953aa09b 100644 --- a/src/shapes/path_group.class.js +++ b/src/shapes/path_group.class.js @@ -17,6 +17,7 @@ * @class fabric.PathGroup * @extends fabric.Path * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} + * @see {@link fabric.PathGroup#initialize} for constructor definition */ fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @lends fabric.PathGroup.prototype */ { diff --git a/src/shapes/polygon.class.js b/src/shapes/polygon.class.js index 2ba4da20..c111d7bd 100644 --- a/src/shapes/polygon.class.js +++ b/src/shapes/polygon.class.js @@ -17,6 +17,7 @@ * Polygon class * @class fabric.Polygon * @extends fabric.Object + * @see {@link fabric.Polygon#initialize} for constructor definition */ fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ { @@ -176,18 +177,9 @@ options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES), - minX = min(points, 'x'), - minY = min(points, 'y'); + parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); - minX = minX < 0 ? minX : 0; - minY = minX < 0 ? minY : 0; - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2 + minX) || 0; - points[i].y -= (options.height / 2 + minY) || 0; - } + fabric.util.normalizePoints(points, options); return new fabric.Polygon(points, extend(parsedAttributes, options), true); }; diff --git a/src/shapes/polyline.class.js b/src/shapes/polyline.class.js index acb85b85..b6f8f32a 100644 --- a/src/shapes/polyline.class.js +++ b/src/shapes/polyline.class.js @@ -3,8 +3,7 @@ "use strict"; var fabric = global.fabric || (global.fabric = { }), - toFixed = fabric.util.toFixed, - min = fabric.util.array.min; + toFixed = fabric.util.toFixed; if (fabric.Polyline) { fabric.warn('fabric.Polyline is already defined'); @@ -15,6 +14,7 @@ * Polyline class * @class fabric.Polyline * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition */ fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { @@ -27,10 +27,23 @@ /** * Constructor - * @param {Array} points Array of points + * @param {Array} points Array of points (where each point is an object with x and y) * @param {Object} [options] Options object * @param {Boolean} [skipOffset] Whether points offsetting should be skipped * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); */ initialize: function(points, options, skipOffset) { options = options || { }; @@ -147,18 +160,9 @@ options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES), - minX = min(points, 'x'), - minY = min(points, 'y'); + parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES); - minX = minX < 0 ? minX : 0; - minY = minX < 0 ? minY : 0; - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2 + minX) || 0; - points[i].y -= (options.height / 2 + minY) || 0; - } + fabric.util.normalizePoints(points, options); return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options), true); }; diff --git a/src/shapes/rect.class.js b/src/shapes/rect.class.js index e1252788..d882674b 100644 --- a/src/shapes/rect.class.js +++ b/src/shapes/rect.class.js @@ -18,6 +18,7 @@ * @class fabric.Rect * @extends fabric.Object * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition */ fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { @@ -106,7 +107,7 @@ y = -this.height / 2, w = this.width, h = this.height, - isInPathGroup = this.group && this.group.type !== 'group'; + isInPathGroup = this.group && this.group.type === 'path-group'; ctx.beginPath(); ctx.globalAlpha = isInPathGroup ? (ctx.globalAlpha * this.opacity) : this.opacity; diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index ad737d7f..60cb9ec9 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -34,6 +34,7 @@ * @extends fabric.Object * @return {fabric.Text} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#text} + * @see {@link fabric.Text#initialize} for constructor definition */ fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { @@ -54,6 +55,11 @@ text: true }, + /** + * @private + */ + _reNewline: /\r?\n/, + /** * Retrieves object's fontSize * @method getFontSize @@ -276,7 +282,8 @@ useNative: true, /** - * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) * as well as for history (undo/redo) purposes * @type Array */ @@ -340,7 +347,7 @@ */ _render: function(ctx) { - var isInPathGroup = this.group && this.group.type !== 'group'; + var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); } @@ -361,31 +368,20 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderViaNative: function(ctx) { + var textLines = this.text.split(this._reNewline); this.transform(ctx, fabric.isLikelyNode); this._setTextStyles(ctx); - var textLines = this.text.split(/\r?\n/); - this.width = this._getTextWidth(ctx, textLines); this.height = this._getTextHeight(ctx, textLines); this.clipTo && fabric.util.clipContext(this, ctx); this._renderTextBackground(ctx, textLines); - - if (this.textAlign !== 'left' && this.textAlign !== 'justify') { - ctx.save(); - ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); - } - - ctx.save(); - this._setShadow(ctx); - this._renderTextFill(ctx, textLines); - this._renderTextStroke(ctx, textLines); - this._removeShadow(ctx); - ctx.restore(); + this._translateForTextAlign(ctx); + this._renderText(ctx, textLines); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.restore(); @@ -398,6 +394,30 @@ this._totalLineHeight = 0; }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx, textLines) { + ctx.save(); + this._setShadow(ctx); + this._renderTextFill(ctx, textLines); + this._renderTextStroke(ctx, textLines); + this._removeShadow(ctx); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _translateForTextAlign: function(ctx) { + if (this.textAlign !== 'left' && this.textAlign !== 'justify') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -424,22 +444,12 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _setTextStyles: function(ctx) { - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx) - : this.fill; - } - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx) - : this.stroke; - } + this._setFillStyles(ctx); + this._setStrokeStyles(ctx); ctx.textBaseline = 'alphabetic'; - ctx.textAlign = this.textAlign; + if (!this.skipTextAlign) { + ctx.textAlign = this.textAlign; + } ctx.font = this._getFontDeclaration(); }, @@ -460,7 +470,7 @@ * @return {Number} Maximum width of fabric.Text object */ _getTextWidth: function(ctx, textLines) { - var maxWidth = ctx.measureText(textLines[0]).width; + var maxWidth = ctx.measureText(textLines[0] || '|').width; for (var i = 1, len = textLines.length; i < len; i++) { var currentLineWidth = ctx.measureText(textLines[i]).width; @@ -479,7 +489,7 @@ * @param {Number} left Left position of text * @param {Number} top Top position of text */ - _drawChars: function(method, ctx, chars, left, top) { + _renderChars: function(method, ctx, chars, left, top) { ctx[method](chars, left, top); }, @@ -492,13 +502,13 @@ * @param {Number} top Top position of text * @param {Number} lineIndex Index of a line in a text */ - _drawTextLine: function(method, ctx, line, left, top, lineIndex) { + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // lift the line by quarter of fontSize top -= this.fontSize / 4; // short-circuit if (this.textAlign !== 'justify') { - this._drawChars(method, ctx, line, left, top, lineIndex); + this._renderChars(method, ctx, line, left, top, lineIndex); return; } @@ -515,12 +525,12 @@ var leftOffset = 0; for (var i = 0, len = words.length; i < len; i++) { - this._drawChars(method, ctx, words[i], left + leftOffset, top, lineIndex); + this._renderChars(method, ctx, words[i], left + leftOffset, top, lineIndex); leftOffset += ctx.measureText(words[i]).width + spaceWidth; } } else { - this._drawChars(method, ctx, line, left, top, lineIndex); + this._renderChars(method, ctx, line, left, top, lineIndex); } }, @@ -558,7 +568,7 @@ var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; - this._drawTextLine( + this._renderTextLine( 'fillText', ctx, textLines[i], @@ -593,7 +603,7 @@ var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; - this._drawTextLine( + this._renderTextLine( 'strokeText', ctx, textLines[i], @@ -804,22 +814,43 @@ */ toSVG: function(reviver) { var markup = [ ], - textLines = this.text.split(/\r?\n/), - lineTopOffset = this.useNative + textLines = this.text.split(this._reNewline), + offsets = this._getSVGLeftTopOffsets(textLines), + textAndBg = this._getSVGTextAndBg(offsets.lineTop, offsets.textLeft, textLines), + shadowSpans = this._getSVGShadows(offsets.lineTop, textLines); + + // move top offset by an ascent + offsets.textTop += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + this._wrapSVGTextAndBg(markup, textAndBg, shadowSpans, offsets); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function(textLines) { + var lineTop = this.useNative ? this.fontSize * this.lineHeight : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), - textLeftOffset = -(this.width/2), - textTopOffset = this.useNative + textLeft = -(this.width/2), + textTop = this.useNative ? this.fontSize - 1 - : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight; - textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), - shadowSpans = this._getSVGShadows(lineTopOffset, textLines); - - // move top offset by an ascent - textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + return { + textLeft: textLeft, + textTop: textTop, + lineTop: lineTop + }; + }, + /** + * @private + */ + _wrapSVGTextAndBg: function(markup, textAndBg, shadowSpans, offsets) { markup.push( '', textAndBg.textBgRects.join(''), @@ -831,23 +862,21 @@ (this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ': ''), 'style="', this.getSvgStyles(), '" ', /* svg starts from left/bottom corner so we normalize height */ - 'transform="translate(', toFixed(textLeftOffset, 2), ' ', toFixed(textTopOffset, 2), ')">', + 'transform="translate(', toFixed(offsets.textLeft, 2), ' ', toFixed(offsets.textTop, 2), ')">', shadowSpans.join(''), textAndBg.textSpans.join(''), '', '' ); - - return reviver ? reviver(markup.join('')) : markup.join(''); }, /** * @private - * @param {Number} lineTopOffset Line top offset + * @param {Number} lineHeight * @param {Array} textLines Array of all text lines * @return {Array} */ - _getSVGShadows: function(lineTopOffset, textLines) { + _getSVGShadows: function(lineHeight, textLines) { var shadowSpans = [], i, len, lineTopOffsetMultiplier = 1; @@ -864,14 +893,15 @@ toFixed((lineLeftOffset + lineTopOffsetMultiplier) + this.shadow.offsetX, 2), ((i === 0 || this.useNative) ? '" y' : '" dy'), '="', toFixed(this.useNative - ? ((lineTopOffset * i) - this.height / 2 + this.shadow.offsetY) - : (lineTopOffset + (i === 0 ? this.shadow.offsetY : 0)), 2), + ? ((lineHeight * i) - this.height / 2 + this.shadow.offsetY) + : (lineHeight + (i === 0 ? this.shadow.offsetY : 0)), 2), '" ', this._getFillAttributes(this.shadow.color), '>', fabric.util.string.escapeXml(textLines[i]), ''); lineTopOffsetMultiplier = 1; - } else { + } + else { // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier // prevents empty tspans lineTopOffsetMultiplier++; @@ -883,15 +913,79 @@ /** * @private - * @param {Number} lineTopOffset Line top offset + * @param {Number} lineHeight * @param {Number} textLeftOffset Text left offset * @param {Array} textLines Array of all text lines * @return {Object} */ - _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { - var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; + _getSVGTextAndBg: function(lineHeight, textLeftOffset, textLines) { + var textSpans = [ ], + textBgRects = [ ], + lineTopOffsetMultiplier = 1; // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + this._setSVGTextLineText(textLines[i], i, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + lineTopOffsetMultiplier = 1; + } + else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + + if (!this.textBackgroundColor || !this._boundaries) continue; + + this._setSVGTextLineBg(textBgRects, i, textLeftOffset, lineHeight); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + _setSVGTextLineText: function(textLine, i, textSpans, lineHeight, lineTopOffsetMultiplier) { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) + ? toFixed(this._boundaries[i].left, 2) + : 0; + + textSpans.push( + ' elements since setting opacity + // on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLine), + '' + ); + }, + + _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) { + textBgRects.push( + ''); + }, + + _setSVGBg: function(textBgRects) { if (this.backgroundColor && this._boundaries) { textBgRects.push( ''); } - - // text and text-background - for (i = 0, len = textLines.length; i < len; i++) { - if (textLines[i] !== '') { - lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; - textSpans.push( - ' elements since setting opacity on containing one doesn't work in Illustrator - this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(textLines[i]), - '' - ); - lineTopOffsetMultiplier = 1; - } - else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - - if (!this.textBackgroundColor || !this._boundaries) continue; - - textBgRects.push( - ''); - } - return { - textSpans: textSpans, - textBgRects: textBgRects - }; }, /** diff --git a/src/shapes/triangle.class.js b/src/shapes/triangle.class.js index e0018234..36ed6636 100644 --- a/src/shapes/triangle.class.js +++ b/src/shapes/triangle.class.js @@ -14,6 +14,7 @@ * @class fabric.Triangle * @extends fabric.Object * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition */ fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index a14794eb..0f967ea7 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -11,7 +11,6 @@ var extend = fabric.util.object.extend, getElementOffset = fabric.util.getElementOffset, removeFromArray = fabric.util.removeFromArray, - removeListener = fabric.util.removeListener, CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); @@ -21,6 +20,7 @@ * @mixes fabric.Collection * @mixes fabric.Observable * @see {@link http://fabricjs.com/static_canvas/|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition * @fires before:render * @fires after:render * @fires canvas:cleared @@ -43,56 +43,43 @@ }, /** - * Background color of canvas instance - * @type String + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} * @default */ backgroundColor: '', /** - * Background image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setBackgroundImage} - * @type String + * Background image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. + * Backwards incompatibility note: The "backgroundImageOpacity" + * and "backgroundImageStretch" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. + * @type fabric.Image * @default */ - backgroundImage: '', + backgroundImage: null, /** - * Opacity of the background image of the canvas instance - * @type Float + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} * @default */ - backgroundImageOpacity: 1, + overlayColor: '', /** - * Indicates whether the background image should be stretched to fit the - * dimensions of the canvas instance. - * @type Boolean + * Overlay image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. + * Backwards incompatibility note: The "overlayImageLeft" + * and "overlayImageTop" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#left} and {@link fabric.Image#top}. + * @type fabric.Image * @default */ - backgroundImageStretch: true, - - /** - * Overlay image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setOverlayImage} - * @type String - * @default - */ - overlayImage: '', - - /** - * Left offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageLeft: 0, - - /** - * Top offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageTop: 0, + overlayImage: null, /** * Indicates whether toObject/toDatalessObject should include default values @@ -174,6 +161,9 @@ if (options.backgroundColor) { this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, this.renderAll.bind(this)); + } this.calcOffset(); }, @@ -190,74 +180,115 @@ /** * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {String} url url of an image to set overlay to + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the overlay image - * @param {Number} [options.overlayImageLeft] {@link fabric.StaticCanvas#overlayImageLeft|Left offset} of overlay image - * @param {Number} [options.overlayImageTop] {@link fabric.StaticCanvas#overlayImageTop|Top offset} of overlay image + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Displaced overlayImage (left and top != 0) + * @example Normal overlayImage with left/top = 0 * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * overlayImageLeft: 100, - * overlayImageTop: 100 + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setOverlayImage: function (url, callback, options) { // TODO (kangax): test callback - fabric.util.loadImage(url, function(img) { - this.overlayImage = img; - if (options && ('overlayImageLeft' in options)) { - this.overlayImageLeft = options.overlayImageLeft; - } - if (options && ('overlayImageTop' in options)) { - this.overlayImageTop = options.overlayImageTop; - } - callback && callback(); - }, this); - - return this; + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); }, /** * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {String} url URL of an image to set background to - * @param {Function} callback callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the background image - * @param {Float} [options.backgroundImageOpacity] {@link fabric.StaticCanvas#backgroundImageOpacity|Opacity} of the background image of the canvas instance - * @param {Boolean} [options.backgroundImageStretch] Indicates whether the background image should be {@link fabric.StaticCanvas#backgroundImageStretch|strechted} to fit the canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} - * @example Normal backgroundImage - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Stretched backgroundImage with opacity - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * backgroundImageOpacity: 0.5, - * backgroundImageStretch: true + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setBackgroundImage: function (url, callback, options) { - fabric.util.loadImage(url, function(img) { - this.backgroundImage = img; - if (options && ('backgroundImageOpacity' in options)) { - this.backgroundImageOpacity = options.backgroundImageOpacity; - } - if (options && ('backgroundImageStretch' in options)) { - this.backgroundImageStretch = options.backgroundImageStretch; - } - callback && callback(); - }, this); + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, - return this; + /** + * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); }, /** * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {String|fabric.Pattern} backgroundColor Color or pattern to set background color to - * @param {Function} callback callback to invoke when background color is set + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} @@ -267,20 +298,63 @@ * canvas.setBackgroundColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png' * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); */ setBackgroundColor: function(backgroundColor, callback) { - if (backgroundColor.source) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img) { + this[property] = new fabric.Image(img, options); + callback && callback(); + }, this); + } + else { + this[property] = image; + callback && callback(); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String)} color Object with pattern information or color value + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + if (color.source) { var _this = this; - fabric.util.loadImage(backgroundColor.source, function(img) { - _this.backgroundColor = new fabric.Pattern({ + fabric.util.loadImage(color.source, function(img) { + _this[property] = new fabric.Pattern({ source: img, - repeat: backgroundColor.repeat + repeat: color.repeat, + offsetX: color.offsetX, + offsetY: color.offsetY }); callback && callback(); }); } else { - this.backgroundColor = backgroundColor; + this[property] = color; callback && callback(); } @@ -589,18 +663,17 @@ * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (this.getActiveObject() === obj) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared'); + } + this.fire('object:removed', { target: obj }); obj.fire('removed'); }, - /** - * Returns an array of objects this instance has - * @return {Array} - */ - getObjects: function () { - return this._objects; - }, - /** * Clears specified context of canvas element * @param {CanvasRenderingContext2D} ctx Context to clear @@ -651,6 +724,7 @@ renderAll: function (allOnTop) { var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + var activeGroup = this.getActiveGroup(); if (this.contextTop && this.selection && !this._groupSelector) { this.clearContext(this.contextTop); @@ -666,50 +740,15 @@ fabric.util.clipContext(this, canvasToDrawOn); } - if (this.backgroundColor) { - canvasToDrawOn.fillStyle = this.backgroundColor.toLive - ? this.backgroundColor.toLive(canvasToDrawOn) - : this.backgroundColor; - - canvasToDrawOn.fillRect( - this.backgroundColor.offsetX || 0, - this.backgroundColor.offsetY || 0, - this.width, - this.height); - } - - if (typeof this.backgroundImage === 'object') { - this._drawBackroundImage(canvasToDrawOn); - } - - var activeGroup = this.getActiveGroup(); - for (var i = 0, length = this._objects.length; i < length; ++i) { - if (!activeGroup || - (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { - this._draw(canvasToDrawOn, this._objects[i]); - } - } - - // delegate rendering to group selection (if one exists) - if (activeGroup) { - //Store objects in group preserving order, then replace - var sortedObjects = []; - this.forEachObject(function (object) { - if (activeGroup.contains(object)) { - sortedObjects.push(object); - } - }); - activeGroup._set('objects', sortedObjects); - this._draw(canvasToDrawOn, activeGroup); - } + this._renderBackground(canvasToDrawOn); + this._renderObjects(canvasToDrawOn, activeGroup); + this._renderActiveGroup(canvasToDrawOn, activeGroup); if (this.clipTo) { canvasToDrawOn.restore(); } - if (this.overlayImage) { - canvasToDrawOn.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop); - } + this._renderOverlay(canvasToDrawOn); if (this.controlsAboveOverlay && this.interactive) { this.drawControls(canvasToDrawOn); @@ -722,19 +761,80 @@ /** * @private - * @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup */ - _drawBackroundImage: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + _renderObjects: function(ctx, activeGroup) { + for (var i = 0, length = this._objects.length; i < length; ++i) { + if (!activeGroup || + (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { + this._draw(ctx, this._objects[i]); + } + } + }, - if (this.backgroundImageStretch) { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderActiveGroup: function(ctx, activeGroup) { + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + + //Store objects in group preserving order, then replace + var sortedObjects = []; + this.forEachObject(function (object) { + if (activeGroup.contains(object)) { + sortedObjects.push(object); + } + }); + activeGroup._set('objects', sortedObjects); + this._draw(ctx, activeGroup); } - else { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (this.backgroundColor) { + ctx.fillStyle = this.backgroundColor.toLive + ? this.backgroundColor.toLive(ctx) + : this.backgroundColor; + + ctx.fillRect( + this.backgroundColor.offsetX || 0, + this.backgroundColor.offsetY || 0, + this.width, + this.height); + } + if (this.backgroundImage) { + this.backgroundImage.render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + if (this.overlayColor) { + ctx.fillStyle = this.overlayColor.toLive + ? this.overlayColor.toLive(ctx) + : this.overlayColor; + + ctx.fillRect( + this.overlayColor.offsetX || 0, + this.overlayColor.offsetY || 0, + this.width, + this.height); + } + if (this.overlayImage) { + this.overlayImage.render(ctx); } - canvasToDrawOn.restore(); }, /** @@ -783,11 +883,11 @@ /** * Centers object horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - object.set('left', this.getCenter().left); + this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); this.renderAll(); return this; }, @@ -795,12 +895,12 @@ /** * Centers object vertically. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center vertically * @return {fabric.Canvas} thisArg * @chainable */ centerObjectV: function (object) { - object.set('top', this.getCenter().top); + this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); this.renderAll(); return this; }, @@ -808,12 +908,28 @@ /** * Centers object vertically and horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. - * @param {fabric.Object} object Object to center + * @param {fabric.Object} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ - centerObject: function (object) { - return this.centerObjectH(object).centerObjectV(object); + centerObject: function(object) { + var center = this.getCenter(); + + this._centerObject(object, new fabric.Point(center.left, center.top)); + this.renderAll(); + return this; + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + return this; }, /** @@ -852,42 +968,75 @@ if (activeGroup) { this.discardActiveGroup(); } + var data = { - objects: this.getObjects().map(function (instance) { - // TODO (kangax): figure out how to clean this up - var originalValue; - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } - var object = instance[methodName](propertiesToInclude); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, this), - background: (this.backgroundColor && this.backgroundColor.toObject) - ? this.backgroundColor.toObject() - : this.backgroundColor + objects: this._toObjects(methodName, propertiesToInclude) }; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.src; - data.backgroundImageOpacity = this.backgroundImageOpacity; - data.backgroundImageStretch = this.backgroundImageStretch; - } - if (this.overlayImage) { - data.overlayImage = this.overlayImage.src; - data.overlayImageLeft = this.overlayImageLeft; - data.overlayImageTop = this.overlayImageTop; - } + + extend(data, this.__serializeBgOverlay()); + fabric.util.populateWithProperties(this, data, propertiesToInclude); + if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); - activeGroup.forEachObject(function(o) { o.set('active', true) }); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); } return data; }, + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this.getObjects().map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function() { + var data = { + background: (this.backgroundColor && this.backgroundColor.toObject) + ? this.backgroundColor.toObject() + : this.backgroundColor + }; + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject() + : this.overlayColor; + } + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.toObject(); + } + if (this.overlayImage) { + data.overlayImage = this.overlayImage.toObject(); + } + + return data; + }, + /* _TO_SVG_START_ */ /** * Returns SVG representation of canvas @@ -926,8 +1075,29 @@ */ toSVG: function(options, reviver) { options || (options = { }); + var markup = []; + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + + this._setSVGBgOverlayColor(markup, 'backgroundColor'); + this._setSVGBgOverlayImage(markup, 'backgroundImage'); + + this._setSVGObjects(markup, reviver); + + this._setSVGBgOverlayColor(markup, 'overlayColor'); + this._setSVGBgOverlayImage(markup, 'overlayImage'); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { if (!options.suppressPreamble) { markup.push( '', @@ -935,53 +1105,42 @@ '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' ); } + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { markup.push( - '', - 'Created with Fabric.js ', fabric.version, '', - '', fabric.createSVGFontFacesMarkup(this.getObjects()), fabric.createSVGRefElementsMarkup(this), '' + '', + 'Created with Fabric.js ', fabric.version, '', + '', + fabric.createSVGFontFacesMarkup(this.getObjects()), + fabric.createSVGRefElementsMarkup(this), + '' ); + }, - if (this.backgroundColor && this.backgroundColor.source) { - markup.push( - '' - ); - } - - if (this.backgroundImage) { - markup.push( - '' - ); - } - - if (this.overlayImage) { - markup.push( - '' - ); - } - + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { var activeGroup = this.getActiveGroup(); if (activeGroup) { this.discardActiveGroup(); @@ -991,30 +1150,52 @@ } if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); - activeGroup.forEachObject(function(o) { o.set('active', true) }); + activeGroup.forEachObject(function(o) { + o.set('active', true); + }); } - markup.push(''); - - return markup.join(''); }, - /* _TO_SVG_END_ */ /** - * Removes an object from canvas and returns it - * @param {fabric.Object} object Object to remove - * @return {fabric.Object} removed object + * @private */ - remove: function (object) { - // removing active object should fire "selection:cleared" events - if (this.getActiveObject() === object) { - this.fire('before:selection:cleared', { target: object }); - this.discardActiveObject(); - this.fire('selection:cleared'); + _setSVGBgOverlayImage: function(markup, property) { + if (this[property] && this[property].toSVG) { + markup.push(this[property].toSVG()); } - - return fabric.Collection.remove.call(this, object); }, + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + if (this[property] && this[property].source) { + markup.push( + '' + ); + } + else if (this[property] && property === 'overlayColor') { + markup.push( + '' + ); + } + }, + /* _TO_SVG_END_ */ + /** * Moves an object to the bottom of the stack of drawn objects * @param {fabric.Object} object Object to send to back @@ -1051,27 +1232,7 @@ // if object is not on the bottom of stack if (idx !== 0) { - var newIdx; - - if (intersecting) { - newIdx = idx; - - // traverse down the stack looking for the nearest intersecting object - for (var i=idx-1; i>=0; --i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx-1; - } + var newIdx = this._findNewLowerIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); @@ -1080,6 +1241,35 @@ return this; }, + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (var i=idx-1; i>=0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + /** * Moves an object up in stack of drawn objects * @param {fabric.Object} object Object to send @@ -1092,27 +1282,7 @@ // if object is not on top of stack (last item in an array) if (idx !== this._objects.length-1) { - var newIdx; - - if (intersecting) { - newIdx = idx; - - // traverse up the stack looking for the nearest intersecting object - for (var i = idx + 1; i < this._objects.length; ++i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx+1; - } + var newIdx = this._findNewUpperIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); @@ -1121,6 +1291,35 @@ return this; }, + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (var i = idx + 1; i < this._objects.length; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx+1; + } + + return newIdx; + }, + /** * Moves an object to specified level in stack of drawn objects * @param {fabric.Object} object Object to send @@ -1135,27 +1334,13 @@ }, /** - * Clears a canvas element and removes all event handlers. + * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { this.clear(); - - if (!this.interactive) return this; - - if (fabric.isTouchSupported) { - removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - if (typeof Event !== 'undefined' && 'remove' in Event) { - Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); - } - } - else { - removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(fabric.window, 'resize', this._onResize); - } + this.interactive && this.removeListeners(); return this; }, diff --git a/src/util/anim_ease.js b/src/util/anim_ease.js index 41879f25..bed3b7e9 100644 --- a/src/util/anim_ease.js +++ b/src/util/anim_ease.js @@ -1,37 +1,15 @@ (function() { - /** - * Quadratic easing in - * @memberOf fabric.util.ease - */ - function easeInQuad(t, b, c, d) { - return c*(t/=d)*t + b; + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return { a: a, c: c, p: p, s: s }; } - /** - * Quadratic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuad(t, b, c, d) { - return -c *(t/=d)*(t-2) + b; - } - - /** - * Quadratic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuad(t, b, c, d) { - t /= (d/2); - if (t < 1) return c/2*t*t + b; - return -c/2 * ((--t)*(t-2) - 1) + b; - } - - /** - * Cubic easing in - * @memberOf fabric.util.ease - */ - function easeInCubic(t, b, c, d) { - return c*(t/=d)*t*t + b; + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); } /** @@ -192,9 +170,8 @@ t /= d; if (t===1) return b+c; if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; } /** @@ -207,9 +184,8 @@ t /= d; if (t===1) return b+c; if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + var opts = normalize(a, c, p, s); + return opts.a*Math.pow(2,-10*t) * Math.sin( (t*d-opts.s)*(2*Math.PI)/opts.p ) + opts.c + b; } /** @@ -222,10 +198,9 @@ t /= d/2; if (t===2) return b+c; if (!p) p=d*(0.3*1.5); - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; + var opts = normalize(a, c, p, s); + if (t < 1) return -0.5 * elastic(opts, t, d) + b; + return opts.a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-opts.s)*(2*Math.PI)/opts.p )*0.5 + opts.c + b; } /** @@ -296,10 +271,41 @@ * @namespace fabric.util.ease */ fabric.util.ease = { - easeInQuad: easeInQuad, - easeOutQuad: easeOutQuad, - easeInOutQuad: easeInOutQuad, - easeInCubic: easeInCubic, + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c*(t/=d)*t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d/2); + if (t < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c*(t/=d)*t*t + b; + }, + easeOutCubic: easeOutCubic, easeInOutCubic: easeInOutCubic, easeInQuart: easeInQuart, diff --git a/src/util/animate.js b/src/util/animate.js index 5cf31dcb..d1bbdbe7 100644 --- a/src/util/animate.js +++ b/src/util/animate.js @@ -14,34 +14,37 @@ */ function animate(options) { - options || (options = { }); + requestAnimFrame(function(timestamp) { + options || (options = { }); - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || endValue - startValue; + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; - options.onStart && options.onStart(); + options.onStart && options.onStart(); + + (function tick(ticktime) { + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start); + if (abort()) { + options.onComplete && options.onComplete(); + return; + } + onChange(easing(currentTime, startValue, byValue, duration)); + if (time > finish) { + options.onComplete && options.onComplete(); + return; + } + requestAnimFrame(tick); + })(start); + }); - (function tick() { - time = +new Date(); - var currentTime = time > finish ? duration : (time - start); - if (abort()) { - options.onComplete && options.onComplete(); - return; - } - onChange(easing(currentTime, startValue, byValue, duration)); - if (time > finish) { - options.onComplete && options.onComplete(); - return; - } - requestAnimFrame(tick); - })(); } var _requestAnimFrame = fabric.window.requestAnimationFrame || @@ -54,6 +57,7 @@ }; /** * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method * @memberOf fabric.util * @param {Function} callback Callback to invoke * @param {DOMElement} element optional Element to associate with animation diff --git a/src/util/arc.js b/src/util/arc.js new file mode 100644 index 00000000..36c567ec --- /dev/null +++ b/src/util/arc.js @@ -0,0 +1,137 @@ +(function() { + + var arcToSegmentsCache = { }, + segmentToBezierCache = { }, + _join = Array.prototype.join, + argsString; + + // Generous contribution by Raph Levien, from libsvg-0.1.0.tar.gz + function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) { + + argsString = _join.call(arguments); + + if (arcToSegmentsCache[argsString]) { + return arcToSegmentsCache[argsString]; + } + + var coords = getXYCoords(rotateX, rx, ry, ox, oy, x, y); + + var d = (coords.x1-coords.x0) * (coords.x1-coords.x0) + + (coords.y1-coords.y0) * (coords.y1-coords.y0); + + var sfactor_sq = 1 / d - 0.25; + if (sfactor_sq < 0) sfactor_sq = 0; + + var sfactor = Math.sqrt(sfactor_sq); + if (sweep === large) sfactor = -sfactor; + + var xc = 0.5 * (coords.x0 + coords.x1) - sfactor * (coords.y1-coords.y0); + var yc = 0.5 * (coords.y0 + coords.y1) + sfactor * (coords.x1-coords.x0); + + var th0 = Math.atan2(coords.y0-yc, coords.x0-xc); + var th1 = Math.atan2(coords.y1-yc, coords.x1-xc); + + var th_arc = th1-th0; + if (th_arc < 0 && sweep === 1) { + th_arc += 2*Math.PI; + } + else if (th_arc > 0 && sweep === 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i 1) { + pl = Math.sqrt(pl); + rx *= pl; + ry *= pl; + } + + var a00 = cos_th / rx; + var a01 = sin_th / rx; + var a10 = (-sin_th) / ry; + var a11 = (cos_th) / ry; + + return { + x0: a00 * ox + a01 * oy, + y0: a10 * ox + a11 * oy, + x1: a00 * x + a01 * y, + y1: a10 * x + a11 * y, + sin_th: sin_th, + cos_th: cos_th + }; + } + + function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) { + argsString = _join.call(arguments); + if (segmentToBezierCache[argsString]) { + return segmentToBezierCache[argsString]; + } + + var a00 = cos_th * rx; + var a01 = -sin_th * ry; + var a10 = sin_th * rx; + var a11 = cos_th * ry; + + var th_half = 0.5 * (th1 - th0); + var t = (8/3) * Math.sin(th_half * 0.5) * + Math.sin(th_half * 0.5) / Math.sin(th_half); + + var x1 = cx + Math.cos(th0) - t * Math.sin(th0); + var y1 = cy + Math.sin(th0) + t * Math.cos(th0); + var x3 = cx + Math.cos(th1); + var y3 = cy + Math.sin(th1); + var x2 = x3 + t * Math.sin(th1); + var y2 = y3 - t * Math.cos(th1); + + segmentToBezierCache[argsString] = [ + a00 * x1 + a01 * y1, a10 * x1 + a11 * y1, + a00 * x2 + a01 * y2, a10 * x2 + a11 * y2, + a00 * x3 + a01 * y3, a10 * x3 + a11 * y3 + ]; + + return segmentToBezierCache[argsString]; + } + + /** + * Draws arc + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Array} coords + */ + fabric.util.drawArc = function(ctx, x, y, coords) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i= result) { - result = array[i][byProperty]; - } - } - } - else { - while (i--) { - if (array[i] >= result) { - result = array[i]; - } - } - } - return result; + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); } /** @@ -222,21 +206,29 @@ * @return {Any} */ function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function find(array, byProperty, condition) { if (!array || array.length === 0) return undefined; var i = array.length - 1, result = byProperty ? array[i][byProperty] : array[i]; - if (byProperty) { while (i--) { - if (array[i][byProperty] < result) { + if (condition(array[i][byProperty], result)) { result = array[i][byProperty]; } } } else { while (i--) { - if (array[i] < result) { + if (condition(array[i], result)) { result = array[i]; } } diff --git a/src/util/lang_string.js b/src/util/lang_string.js index 86df0664..cf4d2239 100644 --- a/src/util/lang_string.js +++ b/src/util/lang_string.js @@ -30,10 +30,14 @@ function camelize(string) { * Capitalizes a string * @memberOf fabric.util.string * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. * @return {String} Capitalized version of a string */ -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); } /** diff --git a/src/util/misc.js b/src/util/misc.js index cdaa840e..1e921ce9 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,581 +1,540 @@ (function(global) { var sqrt = Math.sqrt, - atan2 = Math.atan2; + atan2 = Math.atan2, + PiBy180 = Math.PI / 180; /** * @namespace fabric.util */ - fabric.util = { }; + fabric.util = { - /** - * Removes value from an array. - * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` - * @static - * @memberOf fabric.util - * @param {Array} array - * @param {Any} value - * @return {Array} original array - */ - function removeFromArray(array, value) { - var idx = array.indexOf(value); - if (idx !== -1) { - array.splice(idx, 1); - } - return array; - } + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @param {Array} array + * @param {Any} value + * @return {Array} original array + */ + removeFromArray: function(array, value) { + var idx = array.indexOf(value); + if (idx !== -1) { + array.splice(idx, 1); + } + return array; + }, - /** - * Returns random number between 2 specified ones. - * @static - * @memberOf fabric.util - * @param {Number} min lower limit - * @param {Number} max upper limit - * @return {Number} random value (between min and max) - */ - function getRandomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; - } + /** + * Returns random number between 2 specified ones. + * @static + * @memberOf fabric.util + * @param {Number} min lower limit + * @param {Number} max upper limit + * @return {Number} random value (between min and max) + */ + getRandomInt: function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, - var PiBy180 = Math.PI / 180; + /** + * Transforms degrees to radians. + * @static + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + degreesToRadians: function(degrees) { + return degrees * PiBy180; + }, - /** - * Transforms degrees to radians. - * @static - * @memberOf fabric.util - * @param {Number} degrees value in degrees - * @return {Number} value in radians - */ - function degreesToRadians(degrees) { - return degrees * PiBy180; - } + /** + * Transforms radians to degrees. + * @static + * @memberOf fabric.util + * @param {Number} radians value in radians + * @return {Number} value in degrees + */ + radiansToDegrees: function(radians) { + return radians / PiBy180; + }, - /** - * Transforms radians to degrees. - * @static - * @memberOf fabric.util - * @param {Number} radians value in radians - * @return {Number} value in degrees - */ - function radiansToDegrees(radians) { - return radians / PiBy180; - } + /** + * Rotates `point` around `origin` with `radians` + * @static + * @memberOf fabric.util + * @param {fabric.Point} The point to rotate + * @param {fabric.Point} The origin of the rotation + * @param {Number} The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + var sin = Math.sin(radians), + cos = Math.cos(radians); - /** - * Rotates `point` around `origin` with `radians` - * @static - * @memberOf fabric.util - * @param {fabric.Point} The point to rotate - * @param {fabric.Point} The origin of the rotation - * @param {Number} The radians of the angle for the rotation - * @return {fabric.Point} The new rotated point - */ - function rotatePoint(point, origin, radians) { - var sin = Math.sin(radians), - cos = Math.cos(radians); + point.subtractEquals(origin); - point.subtractEquals(origin); + var rx = point.x * cos - point.y * sin, + ry = point.x * sin + point.y * cos; - var rx = point.x * cos - point.y * sin; - var ry = point.x * sin + point.y * cos; + return new fabric.Point(rx, ry).addEquals(origin); + }, - return new fabric.Point(rx, ry).addEquals(origin); - } - - /** - * Apply transform t to point p - * @static - * @memberOf fabric.util - * @param {fabric.Point} p The point to transform - * @param {Array} t The transform - * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied - * @return {fabric.Point} The transformed point - */ - function transformPoint(p, t, ignoreOffset) { - if (ignoreOffset) { + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[1] * p.y, + t[2] * p.x + t[3] * p.y + ); + } return new fabric.Point( - t[0] * p.x + t[1] * p.y, - t[2] * p.x + t[3] * p.y + t[0] * p.x + t[1] * p.y + t[4], + t[2] * p.x + t[3] * p.y + t[5] ); - } - return new fabric.Point( - t[0] * p.x + t[1] * p.y + t[4], - t[2] * p.x + t[3] * p.y + t[5] - ); - } - - /** - * Invert transformation t - * @static - * @memberOf fabric.util - * @param {Array} t The transform - * @return {Array} The inverted transform - */ - function invertTransform(t) { - var r = t.slice(), - a = 1 / (t[0] * t[3] - t[1] * t[2]); - r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; - var o = transformPoint({x: t[4], y: t[5]}, r); - r[4] = -o.x; - r[5] = -o.y; - return r - } + }, - /** - * A wrapper around Number#toFixed, which contrary to native method returns number, not string. - * @static - * @memberOf fabric.util - * @param {Number | String} number number to operate on - * @param {Number} fractionDigits number of fraction digits to "leave" - * @return {Number} - */ - function toFixed(number, fractionDigits) { - return parseFloat(Number(number).toFixed(fractionDigits)); - } + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var r = t.slice(), + a = 1 / (t[0] * t[3] - t[1] * t[2]); + r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; + var o = fabric.util.transformPoint({x: t[4], y: t[5]}, r); + r[4] = -o.x; + r[5] = -o.y; + return r + }, - /** - * Function which always returns `false`. - * @static - * @memberOf fabric.util - * @return {Boolean} - */ - function falseFunction() { - return false; - } + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @memberOf fabric.util + * @param {Number | String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, - /** - * Returns klass "Class" object of given namespace - * @memberOf fabric.util - * @param {String} type Type of object (eg. 'circle') - * @param {String} namespace Namespace to get klass "Class" object from - * @return {Object} klass "Class" - */ - function getKlass(type, namespace) { - // capitalize first letter only - type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); - return resolveNamespace(namespace)[type]; - } + /** + * Function which always returns `false`. + * @static + * @memberOf fabric.util + * @return {Boolean} + */ + falseFunction: function() { + return false; + }, - /** - * Returns object of given namespace - * @memberOf fabric.util - * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' - * @return {Object} Object for given namespace (default fabric) - */ - function resolveNamespace(namespace) { - if (!namespace) return fabric; + /** + * Returns klass "Class" object of given namespace + * @memberOf fabric.util + * @param {String} type Type of object (eg. 'circle') + * @param {String} namespace Namespace to get klass "Class" object from + * @return {Object} klass "Class" + */ + getKlass: function(type, namespace) { + // capitalize first letter only + type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); + return fabric.util.resolveNamespace(namespace)[type]; + }, - var parts = namespace.split('.'), - len = parts.length, - obj = global || fabric.window; + /** + * Returns object of given namespace + * @memberOf fabric.util + * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' + * @return {Object} Object for given namespace (default fabric) + */ + resolveNamespace: function(namespace) { + if (!namespace) return fabric; - for (var i = 0; i < len; ++i) { - obj = obj[parts[i]]; - } + var parts = namespace.split('.'), + len = parts.length, + obj = global || fabric.window; - return obj; - } + for (var i = 0; i < len; ++i) { + obj = obj[parts[i]]; + } + + return obj; + }, + + /** + * Loads image element from given url and passes it to a callback + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {Any} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } - /** - * Loads image element from given url and passes it to a callback - * @memberOf fabric.util - * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {Any} context optional Context to invoke callback in - */ - function loadImage(url, callback, context) { - if (url) { var img = fabric.util.createImage(); + /** @ignore */ img.onload = function () { callback && callback.call(context, img); - img = img.onload = null; + img = img.onload = img.onerror = null; }; + + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + if (url.indexOf('data') !== 0) { + img.crossOrigin = crossOrigin || ''; + } + img.src = url; - } - else { - callback && callback.call(context, url); - } - } + }, - /** - * Creates corresponding fabric instances from their object representations - * @static - * @memberOf fabric.util - * @param {Array} objects Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * @param {Function} [reviver] Method for further parsing of object elements, called after each fabric object created. - */ - function enlivenObjects(objects, callback, namespace, reviver) { + /** + * Creates corresponding fabric instances from their object representations + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {Function} [reviver] Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || [ ]; - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - if (callback) { - callback(enlivenedObjects); + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects); } } - } - var enlivenedObjects = [ ], - numLoadedObjects = 0, - numTotalObjects = objects.length; + var enlivenedObjects = [ ], + numLoadedObjects = 0, + numTotalObjects = objects.length; - objects.forEach(function (o, index) { - // if sparse array - if (!o || !o.type) { - onLoaded(); + if (!numTotalObjects) { + callback && callback(enlivenedObjects); return; } - var klass = fabric.util.getKlass(o.type, namespace); - if (klass.async) { - klass.fromObject(o, function (obj, error) { - if (!error) { - enlivenedObjects[index] = obj; - reviver && reviver(o, enlivenedObjects[index]); - } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { onLoaded(); - }); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + if (klass.async) { + klass.fromObject(o, function (obj, error) { + if (!error) { + enlivenedObjects[index] = obj; + reviver && reviver(o, enlivenedObjects[index]); + } + onLoaded(); + }); + } + else { + enlivenedObjects[index] = klass.fromObject(o); + reviver && reviver(o, enlivenedObjects[index]); + onLoaded(); + } + }); + }, + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @return {fabric.Object|fabric.PathGroup} + */ + groupSVGElements: function(elements, options, path) { + var object; + + if (elements.length > 1) { + object = new fabric.PathGroup(elements, options); } else { - enlivenedObjects[index] = klass.fromObject(o); - reviver && reviver(o, enlivenedObjects[index]); - onLoaded(); + object = elements[0]; } - }); - } - /** - * Groups SVG elements (usually those retrieved from SVG document) - * @static - * @memberOf fabric.util - * @param {Array} elements SVG elements to group - * @param {Object} [options] Options object - * @return {fabric.Object|fabric.PathGroup} - */ - function groupSVGElements(elements, options, path) { - var object; + if (typeof path !== 'undefined') { + object.setSourcePath(path); + } + return object; + }, - if (elements.length > 1) { - object = new fabric.PathGroup(elements, options); - } - else { - object = elements[0]; - } - - if (typeof path !== 'undefined') { - object.setSourcePath(path); - } - return object; - } - - /** - * Populates an object with properties of another object - * @static - * @memberOf fabric.util - * @param {Object} source Source object - * @param {Object} destination Destination object - * @return {Array} properties Propertie names to include - */ - function populateWithProperties(source, destination, properties) { - if (properties && Object.prototype.toString.call(properties) === '[object Array]') { - for (var i = 0, len = properties.length; i < len; i++) { - if (properties[i] in source) { - destination[properties[i]] = source[properties[i]]; + /** + * Populates an object with properties of another object + * @static + * @memberOf fabric.util + * @param {Object} source Source object + * @param {Object} destination Destination object + * @return {Array} properties Propertie names to include + */ + populateWithProperties: function(source, destination, properties) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + for (var i = 0, len = properties.length; i < len; i++) { + if (properties[i] in source) { + destination[properties[i]] = source[properties[i]]; + } } } - } - } + }, - /** - * Draws a dashed line between two points - * - * This method is used to draw dashed line around selection area. - * See dotted stroke in canvas - * - * @param ctx {Canvas} context - * @param x {Number} start x coordinate - * @param y {Number} start y coordinate - * @param x2 {Number} end x coordinate - * @param y2 {Number} end y coordinate - * @param da {Array} dash array pattern - */ - function drawDashedLine(ctx, x, y, x2, y2, da) { - var dx = x2 - x, - dy = y2 - y, - len = sqrt(dx*dx + dy*dy), - rot = atan2(dy, dx), - dc = da.length, - di = 0, - draw = true; + /** + * Draws a dashed line between two points + * + * This method is used to draw dashed line around selection area. + * See dotted stroke in canvas + * + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x start x coordinate + * @param {Number} y start y coordinate + * @param {Number} x2 end x coordinate + * @param {Number} y2 end y coordinate + * @param {Array} da dash array pattern + */ + drawDashedLine: function(ctx, x, y, x2, y2, da) { + var dx = x2 - x, + dy = y2 - y, + len = sqrt(dx*dx + dy*dy), + rot = atan2(dy, dx), + dc = da.length, + di = 0, + draw = true; - ctx.save(); - ctx.translate(x, y); - ctx.moveTo(0, 0); - ctx.rotate(rot); + ctx.save(); + ctx.translate(x, y); + ctx.moveTo(0, 0); + ctx.rotate(rot); - x = 0; - while (len > x) { - x += da[di++ % dc]; - if (x > len) { - x = len; - } - ctx[draw ? 'lineTo' : 'moveTo'](x, 0); - draw = !draw; - } - - ctx.restore(); - } - - /** - * Creates canvas element and initializes it via excanvas if necessary - * @static - * @memberOf fabric.util - * @param {CanvasElement} [canvasEl] optional canvas element to initialize; when not given, element is created implicitly - * @return {CanvasElement} initialized canvas element - */ - function createCanvasElement(canvasEl) { - canvasEl || (canvasEl = fabric.document.createElement('canvas')); - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - return canvasEl; - } - - /** - * Creates image element (works on client and node) - * @static - * @memberOf fabric.util - * @return {HTMLImageElement} HTML image element - */ - function createImage() { - return fabric.isLikelyNode - ? new (require('canvas').Image)() - : fabric.document.createElement('img'); - } - - /** - * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array - * @static - * @memberOf fabric.util - * @param {Object} klass "Class" to create accessors for - */ - function createAccessors(klass) { - var proto = klass.prototype; - - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - } - - /** - * @static - * @memberOf fabric.util - * @param {fabric.Object} receiver Object implementing `clipTo` method - * @param {CanvasRenderingContext2D} ctx Context to clip - */ - function clipContext(receiver, ctx) { - ctx.save(); - ctx.beginPath(); - receiver.clipTo(ctx); - ctx.clip(); - } - - /** - * Multiply matrix A by matrix B to nest transformations - * @static - * @memberOf fabric.util - * @param {Array} matrixA First transformMatrix - * @param {Array} matrixB Second transformMatrix - * @return {Array} The product of the two transform matrices - */ - function multiplyTransformMatrices(matrixA, matrixB) { - // Matrix multiply matrixA * matrixB - var a = [ - [matrixA[0], matrixA[2], matrixA[4]], - [matrixA[1], matrixA[3], matrixA[5]], - [0 , 0 , 1 ] - ]; - - var b = [ - [matrixB[0], matrixB[2], matrixB[4]], - [matrixB[1], matrixB[3], matrixB[5]], - [0 , 0 , 1 ] - ]; - - var result = []; - for (var r=0; r<3; r++) { - result[r] = []; - for (var c=0; c<3; c++) { - var sum = 0; - for (var k=0; k<3; k++) { - sum += a[r][k]*b[k][c]; + x = 0; + while (len > x) { + x += da[di++ % dc]; + if (x > len) { + x = len; } - - result[r][c] = sum; + ctx[draw ? 'lineTo' : 'moveTo'](x, 0); + draw = !draw; } + + ctx.restore(); + }, + + /** + * Creates canvas element and initializes it via excanvas if necessary + * @static + * @memberOf fabric.util + * @param {CanvasElement} [canvasEl] optional canvas element to initialize; + * when not given, element is created implicitly + * @return {CanvasElement} initialized canvas element + */ + createCanvasElement: function(canvasEl) { + canvasEl || (canvasEl = fabric.document.createElement('canvas')); + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + return canvasEl; + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + createImage: function() { + return fabric.isLikelyNode + ? new (require('canvas').Image)() + : fabric.document.createElement('img'); + }, + + /** + * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array + * @static + * @memberOf fabric.util + * @param {Object} klass "Class" to create accessors for + */ + createAccessors: function(klass) { + var proto = klass.prototype; + + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + }, + + /** + * @static + * @memberOf fabric.util + * @param {fabric.Object} receiver Object implementing `clipTo` method + * @param {CanvasRenderingContext2D} ctx Context to clip + */ + clipContext: function(receiver, ctx) { + ctx.save(); + ctx.beginPath(); + receiver.clipTo(ctx); + ctx.clip(); + }, + + /** + * Multiply matrix A by matrix B to nest transformations + * @static + * @memberOf fabric.util + * @param {Array} matrixA First transformMatrix + * @param {Array} matrixB Second transformMatrix + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(matrixA, matrixB) { + // Matrix multiply matrixA * matrixB + var a = [ + [matrixA[0], matrixA[2], matrixA[4]], + [matrixA[1], matrixA[3], matrixA[5]], + [0 , 0 , 1 ] + ]; + + var b = [ + [matrixB[0], matrixB[2], matrixB[4]], + [matrixB[1], matrixB[3], matrixB[5]], + [0 , 0 , 1 ] + ]; + + var result = []; + for (var r=0; r<3; r++) { + result[r] = []; + for (var c=0; c<3; c++) { + var sum = 0; + for (var k=0; k<3; k++) { + sum += a[r][k]*b[k][c]; + } + + result[r][c] = sum; + } + } + + return [ + result[0][0], + result[1][0], + result[0][1], + result[1][1], + result[0][2], + result[1][2] + ]; + }, + + /** + * Returns string representation of function body + * @param {Function} fn Function to get body of + * @return {String} Function body + */ + getFunctionBody: function(fn) { + return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; + }, + + /** + * Normalizes polygon/polyline points according to their dimensions + * @param {Array} points + * @param {Object} options + */ + normalizePoints: function(points, options) { + var minX = fabric.util.array.min(points, 'x'), + minY = fabric.util.array.min(points, 'y'); + + minX = minX < 0 ? minX : 0; + minY = minX < 0 ? minY : 0; + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box + // (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2 + minX) || 0; + points[i].y -= (options.height / 2 + minY) || 0; + } + }, + + /** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Number} tolerance Tolerance + */ + isTransparent: function(ctx, x, y, tolerance) { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + var _isTransparent = true; + var imageData = ctx.getImageData( + x, y, (tolerance * 2) || 1, (tolerance * 2) || 1); + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (var i = 3, l = imageData.data.length; i < l; i += 4) { + var temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) break; // Stop if colour found + } + + imageData = null; + + return _isTransparent; } - - return [ - result[0][0], - result[1][0], - result[0][1], - result[1][1], - result[0][2], - result[1][2] - ]; - } - - function getFunctionBody(fn) { - return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; - } - - function drawArc(ctx, x, y, coords) { - var rx = coords[0]; - var ry = coords[1]; - var rot = coords[2]; - var large = coords[3]; - var sweep = coords[4]; - var ex = coords[5]; - var ey = coords[6]; - var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); - for (var i=0; i 1) { - pl = Math.sqrt(pl); - rx *= pl; - ry *= pl; - } - - var a00 = cos_th / rx; - var a01 = sin_th / rx; - var a10 = (-sin_th) / ry; - var a11 = (cos_th) / ry; - var x0 = a00 * ox + a01 * oy; - var y0 = a10 * ox + a11 * oy; - var x1 = a00 * x + a01 * y; - var y1 = a10 * x + a11 * y; - - var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); - var sfactor_sq = 1 / d - 0.25; - if (sfactor_sq < 0) sfactor_sq = 0; - var sfactor = Math.sqrt(sfactor_sq); - if (sweep === large) sfactor = -sfactor; - var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); - var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); - - var th0 = Math.atan2(y0-yc, x0-xc); - var th1 = Math.atan2(y1-yc, x1-xc); - - var th_arc = th1-th0; - if (th_arc < 0 && sweep === 1){ - th_arc += 2*Math.PI; - } else if (th_arc > 0 && sweep === 0) { - th_arc -= 2 * Math.PI; - } - - var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); - var result = []; - for (var i=0; iCreated with Fabric.js ' + fabric.version + ''; - var PATH_JSON = '{"objects": [{"type": "path", "originX": "center", "originY": "center", "left": 268, "top": 266, "width": 51, "height": 49,'+ + var PATH_JSON = '{"objects": [{"type": "path", "originX": "left", "originY": "top", "left": 268, "top": 266, "width": 51, "height": 49,'+ ' "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 1, "scaleX": 1, "scaleY": 1, '+ '"angle": 0, "flipX": false, "flipY": false, "opacity": 1, "path": [["M", 18.511, 13.99],'+ ' ["c", 0, 0, -2.269, -4.487, -12.643, 4.411], ["c", 0, 0, 4.824, -14.161, 19.222, -9.059],'+ @@ -23,23 +23,108 @@ ' -3.56, 6.891, -7.481, 8.848], ["c", -4.689, 2.336, -9.084, -0.802, -11.277, -2.868], ["l",'+ ' -1.948, 3.104], ["l", -1.628, -1.333], ["l", 3.138, -4.689], ["c", 0.025, 0, 9, 1.932, 9, 1.932], '+ '["c", 0.877, -9.979, 2.893, -12.905, 4.942, -15.621], ["C", 17.878, 21.775, 18.713, 17.397, 18.511, '+ - '13.99], ["z", null]]}], "background": "#ff5555"}'; + '13.99], ["z", null]]}], "background": "#ff5555", "overlay":"rgba(0,0,0,0.2)"}'; - var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"center","originY":"center","left":200,"top":200,"width":200,"height":200,"fill":"rgb(0,0,0)",'+ + var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"left","originY":"top","left":200,"top":200,"width":200,"height":200,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","path":"http://example.com/","pathOffset":{"x":100,"y":100}}],"background":""}'; - var RECT_JSON = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",'+ + var RECT_JSON = '{"objects":[{"type":"rect","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ - '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555"}'; + '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; - var RECT_JSON_WITH_PADDING = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":20,"fill":"rgb(0,0,0)",'+ + var RECT_JSON_WITH_PADDING = '{"objects":[{"type":"rect","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":20,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","padding":123,"foo":"bar","rx":0,"ry":0,"x":0,"y":0}],"background":""}'; + function getAbsolutePath(path) { + var isAbsolute = /^https?:/.test(path); + if (isAbsolute) return path; + var imgEl = _createImageElement(); + imgEl.src = path; + var src = imgEl.src; + imgEl = null; + return src; + } + + var IMG_SRC = fabric.isLikelyNode ? (__dirname + '/../fixtures/test_image.gif') : getAbsolutePath('../fixtures/test_image.gif'), + IMG_WIDTH = 276, + IMG_HEIGHT = 110; + + var REFERENCE_IMG_OBJECT = { + 'type': 'image', + 'originX': 'left', + 'originY': 'top', + 'left': 0, + 'top': 0, + 'width': IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects + 'height': IMG_HEIGHT, // or does it now? + 'fill': 'rgb(0,0,0)', + 'stroke': null, + 'strokeWidth': 1, + 'strokeDashArray': null, + 'strokeLineCap': 'butt', + 'strokeLineJoin': 'miter', + 'strokeMiterLimit': 10, + 'scaleX': 1, + 'scaleY': 1, + 'angle': 0, + 'flipX': false, + 'flipY': false, + 'opacity': 1, + 'src': fabric.isLikelyNode ? undefined : IMG_SRC, + 'shadow': null, + 'visible': true, + 'backgroundColor': '', + 'clipTo': null, + 'filters': [], + 'crossOrigin': '' + }; + + function _createImageElement() { + return fabric.isLikelyNode ? new (require('canvas').Image) : fabric.document.createElement('img'); + } + + function _createImageObject(width, height, callback) { + var elImage = _createImageElement(); + elImage.width = width; + elImage.height = height; + setSrc(elImage, IMG_SRC, function() { + callback(new fabric.Image(elImage)); + }); + } + + function createImageObject(callback) { + return _createImageObject(IMG_WIDTH, IMG_HEIGHT, callback); + } + + function setSrc(img, src, callback) { + if (fabric.isLikelyNode) { + require('fs').readFile(src, function(err, imgData) { + if (err) throw err; + img.src = imgData; + callback && callback(); + }); + } + else { + img.src = src; + callback && callback(); + } + } + + function fixImageDimension(imgObj) { + // workaround for node-canvas sometimes producing images with width/height and sometimes not + if (imgObj.width === 0) { + imgObj.width = IMG_WIDTH; + } + if (imgObj.height === 0) { + imgObj.height = IMG_HEIGHT; + } + } + // force creation of static canvas // TODO: fix this var Canvas = fabric.Canvas; @@ -61,6 +146,7 @@ teardown: function() { canvas.clear(); canvas.backgroundColor = fabric.StaticCanvas.prototype.backgroundColor; + canvas.overlayColor = fabric.StaticCanvas.prototype.overlayColor; canvas.calcOffset(); } }); @@ -76,6 +162,19 @@ equal(canvas.getObjects().length, 0, 'should have a 0 length when empty'); }); + test('getObjects with type', function() { + + var rect = new fabric.Rect({ width: 10, height: 20 }); + var circle = new fabric.Circle({ radius: 30 }); + + canvas.add(rect, circle); + + equal(canvas.getObjects().length, 2, 'should have length=2 initially'); + + deepEqual(canvas.getObjects('rect'), [rect], 'should return rect only'); + deepEqual(canvas.getObjects('circle'), [circle], 'should return circle only'); + }); + test('getElement', function() { ok(typeof canvas.getElement == 'function', 'should respond to `getElement` method'); equal(canvas.getElement(), lowerCanvasEl, 'should return a proper element'); @@ -242,7 +341,7 @@ var rect = makeRect({ left: 102, top: 202 }); canvas.add(rect); equal(canvas.centerObjectH(rect), canvas, 'should be chainable'); - equal(rect.get('left'), lowerCanvasEl.width / 2, 'object\'s "left" property should correspond to canvas element\'s center'); + equal(rect.getCenterPoint().x, lowerCanvasEl.width / 2, 'object\'s "left" property should correspond to canvas element\'s center'); }); test('centerObjectV', function() { @@ -250,7 +349,7 @@ var rect = makeRect({ left: 102, top: 202 }); canvas.add(rect); equal(canvas.centerObjectV(rect), canvas, 'should be chainable'); - equal(rect.get('top'), lowerCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center'); + equal(rect.getCenterPoint().y, lowerCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center'); }); test('centerObject', function() { @@ -259,13 +358,13 @@ canvas.add(rect); equal(canvas.centerObject(rect), canvas, 'should be chainable'); - equal(rect.get('top'), lowerCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center'); - equal(rect.get('left'), lowerCanvasEl.height / 2, 'object\'s "left" property should correspond to canvas element\'s center'); + equal(rect.getCenterPoint().y, lowerCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center'); + equal(rect.getCenterPoint().x, lowerCanvasEl.height / 2, 'object\'s "left" property should correspond to canvas element\'s center'); }); test('straightenObject', function() { ok(typeof canvas.straightenObject == 'function'); - var rect = makeRect({ angle: 10 }) + var rect = makeRect({ angle: 10 }); canvas.add(rect); equal(canvas.straightenObject(rect), canvas, 'should be chainable'); equal(rect.getAngle(), 0, 'angle should be coerced to 0 (from 10)'); @@ -352,11 +451,62 @@ ok(typeof canvas.toJSON == 'function'); equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":""}'); canvas.backgroundColor = '#ff5555'; - equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":"#ff5555"}', '`background` value should be reflected in json'); + canvas.overlayColor = 'rgba(0,0,0,0.2)'; + equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}', '`background` and `overlay` value should be reflected in json'); canvas.add(makeRect()); deepEqual(JSON.stringify(canvas.toJSON()), RECT_JSON); }); + test('toJSON custom properties non-existence check', function() { + var rect = new fabric.Rect({ width: 10, height: 20 }); + rect.padding = 123; + canvas.add(rect); + rect.foo = 'bar'; + + canvas.bar = 456; + + var data = canvas.toJSON(['padding', 'foo', 'bar', 'baz']); + ok('padding' in data.objects[0]); + ok('foo' in data.objects[0], 'foo shouldn\'t be included if it\'s not in an object'); + ok(!('bar' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); + ok(!('baz' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); + ok(!('foo' in data)); + ok(!('baz' in data)); + ok('bar' in data); + }); + + asyncTest('toJSON backgroundImage', function() { + createImageObject(function(image) { + + canvas.backgroundImage = image; + + var json = canvas.toJSON(); + + fixImageDimension(json.backgroundImage); + deepEqual(json.backgroundImage, REFERENCE_IMG_OBJECT); + + canvas.backgroundImage = null; + + start(); + }); + }); + + asyncTest('toJSON overlayImage', function() { + createImageObject(function(image) { + + canvas.overlayImage = image; + + var json = canvas.toJSON(); + + fixImageDimension(json.overlayImage); + deepEqual(json.overlayImage, REFERENCE_IMG_OBJECT); + + canvas.overlayImage = null; + + start(); + }); + }); + test('toDatalessJSON', function() { var path = new fabric.Path('M 100 100 L 300 100 L 200 300 z', { sourcePath: 'http://example.com/' @@ -458,6 +608,7 @@ ok(!canvas.isEmpty(), 'canvas is not empty'); equal(obj.type, 'path', 'first object is a path object'); equal(canvas.backgroundColor, '#ff5555', 'backgroundColor is populated properly'); + equal(canvas.overlayColor, 'rgba(0,0,0,0.2)', 'overlayColor is populated properly'); equal(obj.get('left'), 268); equal(obj.get('top'), 266); @@ -499,22 +650,19 @@ }); }); - test('toJSON custom properties non-existence check', function() { - var rect = new fabric.Rect({ width: 10, height: 20 }); - rect.padding = 123; - canvas.add(rect); - rect.foo = 'bar'; + asyncTest('loadFromJSON with text', function() { + var json = '{"objects":[{"type":"text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"","fontFamily":"Delicious_500","fontStyle":"","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}],"background":"#ffffff"}'; + canvas.loadFromJSON(json, function() { - canvas.bar = 456; + canvas.renderAll(); - var data = canvas.toJSON(['padding', 'foo', 'bar', 'baz']); - ok('padding' in data.objects[0]); - ok('foo' in data.objects[0], 'foo shouldn\'t be included if it\'s not in an object'); - ok(!('bar' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); - ok(!('baz' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); - ok(!('foo' in data)); - ok(!('baz' in data)); - ok('bar' in data); + equal('text', canvas.item(0).type); + equal(150, canvas.item(0).left); + equal(200, canvas.item(0).top); + equal('NAME HERE', canvas.item(0).text); + + start(); + }); }); test('remove', function() { @@ -895,19 +1043,4 @@ deepEqual(objectsAdded[3], circle3); }); - asyncTest('loadFromJSON with text', function() { - var json = '{"objects":[{"type":"text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"","fontFamily":"Delicious_500","fontStyle":"","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}],"background":"#ffffff"}'; - canvas.loadFromJSON(json, function() { - - canvas.renderAll(); - - equal('text', canvas.item(0).type); - equal(150, canvas.item(0).left); - equal(200, canvas.item(0).top) - equal('NAME HERE', canvas.item(0).text); - - start(); - }); - }); - })(); diff --git a/test/unit/circle.js b/test/unit/circle.js index 2852348a..39429ed9 100644 --- a/test/unit/circle.js +++ b/test/unit/circle.js @@ -80,8 +80,8 @@ var circle = new fabric.Circle(); var defaultProperties = { 'type': 'circle', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 0, diff --git a/test/unit/color.js b/test/unit/color.js index d6638b92..9b25ba07 100644 --- a/test/unit/color.js +++ b/test/unit/color.js @@ -24,6 +24,13 @@ equal(oColor.toHsl(), 'hsl(262,80%,12%)'); }); + test('empty args', function() { + var oColor = new fabric.Color(); + ok(oColor); + ok(oColor instanceof fabric.Color); + equal(oColor.toHex(), '000000'); + }); + test('getSource', function() { var oColor = new fabric.Color('ffffff'); ok(typeof oColor.getSource == 'function'); @@ -306,4 +313,4 @@ oColor.overlayWith(new fabric.Color('rgb(0,0,0)')); equal(oColor.toRgb(), 'rgb(128,128,128)'); }); -})(); \ No newline at end of file +})(); diff --git a/test/unit/ellipse.js b/test/unit/ellipse.js index 6c854ecd..087e2a76 100644 --- a/test/unit/ellipse.js +++ b/test/unit/ellipse.js @@ -23,8 +23,8 @@ var ellipse = new fabric.Ellipse(); var defaultProperties = { 'type': 'ellipse', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 0, diff --git a/test/unit/group.js b/test/unit/group.js index ee4d0af9..a102fc87 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -134,12 +134,12 @@ var expectedObject = { 'type': 'group', - 'originX': 'center', - 'originY': 'center', - 'left': 80, - 'top': 117.5, - 'width': 70, - 'height': 45, + 'originX': 'left', + 'originY': 'top', + 'left': 90, + 'top': 130, + 'width': 80, + 'height': 60, 'fill': 'rgb(0,0,0)', 'stroke': null, 'strokeWidth': 1, @@ -174,10 +174,10 @@ test('toObject without default values', function() { var expectedObject = { 'type': 'group', - 'left': 80, - 'top': 117.5, - 'width': 70, - 'height': 45, + 'left': 90, + 'top': 130, + 'width': 80, + 'height': 60, 'objects': clone.objects }; @@ -299,20 +299,19 @@ test('toObject without default values', function() { test('containsPoint', function() { var group = makeGroupWith2Objects(); + group.set({ originX: 'center', originY: 'center' }).setCoords(); + // Rect #1 top: 100, left: 100, width: 30, height: 10 // Rect #2 top: 120, left: 50, width: 10, height: 40 ok(typeof group.containsPoint == 'function'); - ok(group.containsPoint({ x: 50, y: 120 })); - ok(group.containsPoint({ x: 100, y: 100 })); ok(!group.containsPoint({ x: 0, y: 0 })); group.scale(2); ok(group.containsPoint({ x: 50, y: 120 })); ok(group.containsPoint({ x: 100, y: 160 })); ok(!group.containsPoint({ x: 0, y: 0 })); - ok(!group.containsPoint({ x: 100, y: 170 })); group.scale(1); group.padding = 30; @@ -320,7 +319,6 @@ test('toObject without default values', function() { ok(group.containsPoint({ x: 50, y: 120 })); ok(group.containsPoint({ x: 100, y: 170 })); ok(!group.containsPoint({ x: 0, y: 0 })); - ok(!group.containsPoint({ x: 100, y: 172 })); }); test('forEachObject', function() { @@ -365,7 +363,7 @@ test('toObject without default values', function() { var group = makeGroupWith2Objects(); ok(typeof group.toSVG == 'function'); - var expectedSVG = ''; + var expectedSVG = ''; equal(group.toSVG(), expectedSVG); }); diff --git a/test/unit/image.js b/test/unit/image.js index 72fe1390..bc412498 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -16,8 +16,8 @@ var REFERENCE_IMG_OBJECT = { 'type': 'image', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects @@ -40,7 +40,8 @@ 'visible': true, 'backgroundColor': '', 'clipTo': null, - 'filters': [] + 'filters': [], + 'crossOrigin': '' }; function _createImageElement() { @@ -145,6 +146,36 @@ }); }); + asyncTest('crossOrigin', function() { + createImageObject(function(image) { + equal(image.crossOrigin, '', 'initial crossOrigin value should be set'); + + start(); + + var elImage = _createImageElement(); + elImage.crossOrigin = 'anonymous'; + var image = new fabric.Image(elImage); + equal(image.crossOrigin, '', 'crossOrigin value on an instance takes precedence'); + + var objRepr = image.toObject(); + equal(objRepr.crossOrigin, '', 'toObject should return proper crossOrigin value'); + + var elImage2 = _createImageElement(); + image.setElement(elImage2); + equal(elImage2.crossOrigin, '', 'setElement should set proper crossOrigin on an img element'); + + // fromObject doesn't work on Node :/ + if (fabric.isLikelyNode) { + start(); + return; + } + fabric.Image.fromObject(objRepr, function(img) { + equal(img.crossOrigin, ''); + start(); + }); + }); + }); + // asyncTest('clone', function() { // createImageObject(function(image) { // ok(typeof image.clone == 'function'); diff --git a/test/unit/image_filters.js b/test/unit/image_filters.js index 41966e22..019bfb86 100644 --- a/test/unit/image_filters.js +++ b/test/unit/image_filters.js @@ -16,8 +16,8 @@ var REFERENCE_IMG_OBJECT = { 'type': 'image', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects diff --git a/test/unit/itext.js b/test/unit/itext.js new file mode 100644 index 00000000..771b7d51 --- /dev/null +++ b/test/unit/itext.js @@ -0,0 +1,469 @@ +(function() { + + QUnit.module('fabric.IText'); + + var ITEXT_OBJECT = { + 'type': 'text', + 'originX': 'left', + 'originY': 'top', + 'left': 0, + 'top': 0, + 'width': 20, + 'height': 52, + 'fill': 'rgb(0,0,0)', + 'stroke': null, + 'strokeWidth': 1, + 'strokeDashArray': null, + 'strokeLineCap': 'butt', + 'strokeLineJoin': 'miter', + 'strokeMiterLimit': 10, + 'scaleX': 1, + 'scaleY': 1, + 'angle': 0, + 'flipX': false, + 'flipY': false, + 'opacity': 1, + 'shadow': null, + 'visible': true, + 'clipTo': null, + 'text': 'x', + 'fontSize': 40, + 'fontWeight': 'normal', + 'fontFamily': 'Times New Roman', + 'fontStyle': '', + 'lineHeight': 1.3, + 'textDecoration': '', + 'textAlign': 'left', + 'path': null, + 'backgroundColor': '', + 'textBackgroundColor': '', + 'useNative': true, + styles: { } + }; + + test('constructor', function() { + var iText = new fabric.IText('test'); + + ok(iText instanceof fabric.IText); + ok(iText instanceof fabric.Text); + }); + + test('initial properties', function() { + var iText = new fabric.IText('test'); + ok(iText instanceof fabric.IText); + + equal(iText.text, 'test'); + equal(iText.type, 'i-text'); + deepEqual(iText.styles, { }); + }); + + test('instances', function() { + var iText = new fabric.IText('test'); + var lastInstance = fabric.IText.instances[fabric.IText.instances.length - 1]; + equal(lastInstance, iText); + }); + + test('fromObject', function() { + ok(typeof fabric.IText.fromObject == 'function'); + + var iText = fabric.IText.fromObject(ITEXT_OBJECT); + + ok(iText instanceof fabric.IText); + deepEqual(ITEXT_OBJECT, iText.toObject()); + }); + + test('toObject', function() { + var styles = { + 0: { + 0: { fill: 'red' }, + 1: { textDecoration: 'underline' } + } + }; + var iText = new fabric.IText('test', { + styles: styles + }); + equal(typeof iText.toObject, 'function'); + + var obj = iText.toObject(); + deepEqual(obj.styles, styles); + }); + + test('setSelectionStart', function() { + var iText = new fabric.IText('test'); + + equal(typeof iText.setSelectionStart, 'function'); + + equal(iText.selectionStart, 0); + + iText.setSelectionStart(3); + equal(iText.selectionStart, 3); + equal(iText.selectionEnd, 0); + }); + + test('setSelectionEnd', function() { + var iText = new fabric.IText('test'); + + equal(typeof iText.setSelectionEnd, 'function'); + + equal(iText.selectionEnd, 0); + + iText.setSelectionEnd(3); + equal(iText.selectionEnd, 3); + equal(iText.selectionStart, 0); + }); + + test('get2DCursorLocation', function() { + var iText = new fabric.IText('test\nfoo\nbarbaz'); + var loc = iText.get2DCursorLocation(); + + equal(loc.lineIndex, 0); + equal(loc.charIndex, 0); + + // 'tes|t' + iText.selectionStart = iText.selectionEnd = 3; + var loc = iText.get2DCursorLocation(); + + equal(loc.lineIndex, 0); + equal(loc.charIndex, 3); + + // test + // fo|o + iText.selectionStart = iText.selectionEnd = 7; + var loc = iText.get2DCursorLocation(); + + equal(loc.lineIndex, 1); + equal(loc.charIndex, 2); + + // test + // foo + // barba|z + iText.selectionStart = iText.selectionEnd = 14; + var loc = iText.get2DCursorLocation(); + + equal(loc.lineIndex, 2); + equal(loc.charIndex, 5); + }); + + test('isEmptyStyles', function() { + var iText = new fabric.IText('test'); + ok(iText.isEmptyStyles()); + + var iText = new fabric.IText('test', { + styles: { + 0: { + 0: { } + }, + 1: { + 0: { }, 1: { } + } + } + }); + ok(iText.isEmptyStyles()); + + var iText = new fabric.IText('test', { + styles: { + 0: { + 0: { } + }, + 1: { + 0: { fill: 'red' }, 1: { } + } + } + }); + ok(!iText.isEmptyStyles()); + }); + + test('selectAll', function() { + var iText = new fabric.IText('test'); + + iText.selectAll(); + equal(iText.selectionStart, 0); + equal(iText.selectionEnd, 4); + + iText.selectionStart = 1; + iText.selectionEnd = 2; + + iText.selectAll(); + equal(iText.selectionStart, 0); + equal(iText.selectionEnd, 4); + }); + + test('getSelectedText', function() { + var iText = new fabric.IText('test\nfoobarbaz'); + iText.selectionStart = 1; + iText.selectionEnd = 10; + equal(iText.getSelectedText(), 'est\nfooba'); + + iText.selectionStart = iText.selectionEnd = 3; + equal(iText.getSelectedText(), ''); + }); + + test('enterEditing, exitEditing', function() { + var iText = new fabric.IText('test'); + + equal(typeof iText.enterEditing, 'function'); + equal(typeof iText.exitEditing, 'function'); + + ok(!iText.isEditing); + + iText.enterEditing(); + ok(iText.isEditing); + + iText.exitEditing(); + ok(!iText.isEditing); + }); + + test('insertChars', function() { + var iText = new fabric.IText('test'); + + equal(typeof iText.insertChars, 'function'); + + iText.insertChars('foo_'); + equal(iText.text, 'foo_test'); + + iText.text = 'test'; + iText.selectionStart = iText.selectionEnd = 2; + iText.insertChars('_foo_'); + equal(iText.text, 'te_foo_st'); + + iText.text = 'test'; + iText.selectionStart = 1; + iText.selectionEnd = 3; + iText.insertChars('_foo_'); + equal(iText.text, 't_foo_t'); + }); + + test('insertNewline', function() { + var iText = new fabric.IText('test'); + + equal(typeof iText.insertNewline, 'function'); + + iText.selectionStart = iText.selectionEnd = 2; + iText.insertNewline(); + + equal(iText.text, 'te\nst'); + + iText.text = 'test'; + iText.selectionStart = 1; + iText.selectionEnd = 3; + iText.insertNewline(); + + equal(iText.text, 't\nt'); + }); + + test('selectWord', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.selectWord, 'function'); + + iText.selectWord(1); + equal(iText.selectionStart, 0); // |test| + equal(iText.selectionEnd, 4); + + iText.selectWord(7); + equal(iText.selectionStart, 5); // |foo| + equal(iText.selectionEnd, 8); + }); + + test('selectLine', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.selectLine, 'function'); + + iText.selectLine(6); + equal(iText.selectionStart, 0); // |test foo bar-baz| + equal(iText.selectionEnd, 16); + + iText.selectLine(18); + equal(iText.selectionStart, 17); // |qux| + equal(iText.selectionEnd, 20); + }); + + test('findWordBoundaryLeft', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.findWordBoundaryLeft, 'function'); + + equal(iText.findWordBoundaryLeft(3), 0); // 'tes|t' + equal(iText.findWordBoundaryLeft(20), 17); // 'qux|' + equal(iText.findWordBoundaryLeft(6), 5); // 'f|oo' + equal(iText.findWordBoundaryLeft(11), 9); // 'ba|r-baz' + }); + + test('findWordBoundaryRight', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.findWordBoundaryRight, 'function'); + + equal(iText.findWordBoundaryRight(3), 4); // 'tes|t' + equal(iText.findWordBoundaryRight(17), 20); // '|qux' + equal(iText.findWordBoundaryRight(6), 8); // 'f|oo' + equal(iText.findWordBoundaryRight(11), 16); // 'ba|r-baz' + }); + + test('findLineBoundaryLeft', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.findLineBoundaryLeft, 'function'); + + equal(iText.findLineBoundaryLeft(3), 0); // 'tes|t' + equal(iText.findLineBoundaryLeft(20), 17); // 'qux|' + }); + + test('findLineBoundaryRight', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.findLineBoundaryRight, 'function'); + + equal(iText.findLineBoundaryRight(3), 16); // 'tes|t' + equal(iText.findLineBoundaryRight(17), 20); // '|qux' + }); + + test('getNumNewLinesInSelectedText', function() { + var iText = new fabric.IText('test foo bar-baz\nqux'); + + equal(typeof iText.getNumNewLinesInSelectedText, 'function'); + + equal(iText.getNumNewLinesInSelectedText(), 0); + + iText.selectionStart = 0; + iText.selectionEnd = 20; + + equal(iText.getNumNewLinesInSelectedText(), 1); + }); + + test('getSelectionStyles', function() { + var iText = new fabric.IText('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { textDecoration: 'underline' }, + 2: { textDecoration: 'overline' }, + 4: { textBackgroundColor: '#ffc' } + }, + 1: { + 0: { fill: 'red' }, + 1: { fill: 'green' }, + 2: { fill: 'blue' } + } + } + }); + + equal(typeof iText.getSelectionStyles, 'function'); + + iText.selectionStart = 0; + iText.selectionEnd = 0; + + deepEqual(iText.getSelectionStyles(), { + textDecoration: 'underline' + }); + + iText.selectionStart = 2; + iText.selectionEnd = 2; + + deepEqual(iText.getSelectionStyles(), { + textDecoration: 'overline' + }); + + iText.selectionStart = 17; + iText.selectionStart = 17; + + deepEqual(iText.getSelectionStyles(), { + fill: 'red' + }); + }); + + test('setSelectionStyles', function() { + var iText = new fabric.IText('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { fill: '#112233' }, + 2: { stroke: '#223344' } + } + } + }); + + equal(typeof iText.setSelectionStyles, 'function'); + + iText.setSelectionStyles({ + fill: 'red', + stroke: 'yellow' + }); + + deepEqual(iText.styles[0][0], { + fill: 'red', + stroke: 'yellow' + }); + + iText.selectionStart = 2; + iText.selectionEnd = 2; + + iText.setSelectionStyles({ + fill: '#998877', + stroke: 'yellow' + }); + + deepEqual(iText.styles[0][2], { + fill: '#998877', + stroke: 'yellow' + }); + }); + + test('getCurrentCharFontSize', function() { + var iText = new fabric.IText('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { fontSize: 20 }, + 1: { fontSize: 60 } + } + } + }); + + equal(typeof iText.getCurrentCharFontSize, 'function'); + + equal(iText.getCurrentCharFontSize(0, 0), 20); + equal(iText.getCurrentCharFontSize(0, 1), 20); + equal(iText.getCurrentCharFontSize(0, 2), 60); + equal(iText.getCurrentCharFontSize(1, 0), 40); + }); + + test('getCurrentCharColor', function() { + var iText = new fabric.IText('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { fill: 'red' }, + 1: { fill: 'green' } + } + } + }); + + equal(typeof iText.getCurrentCharColor, 'function'); + + equal(iText.getCurrentCharColor(0, 0), 'red'); + equal(iText.getCurrentCharColor(0, 1), 'red'); + equal(iText.getCurrentCharColor(0, 2), 'green'); + + // or cursor color + equal(iText.getCurrentCharColor(1, 0), '#333'); + }); + + test('toSVG', function() { + var iText = new fabric.IText('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { fill: '#112233' }, + 2: { stroke: '#223344' } + } + } + }); + + equal(typeof iText.toSVG, 'function'); + + // because translate values differ + if (!fabric.isLikelyNode) { + equal(iText.toSVG(), 'test foo bar-bazqux'); + } + + // TODO: more SVG tests here? + }); + +})(); diff --git a/test/unit/line.js b/test/unit/line.js index e91aca43..1c3102e5 100644 --- a/test/unit/line.js +++ b/test/unit/line.js @@ -2,8 +2,8 @@ var LINE_OBJECT = { 'type': 'line', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 12, 'top': 13, 'width': 2, diff --git a/test/unit/object.js b/test/unit/object.js index d044ec7b..7930c9cc 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -150,12 +150,12 @@ }); test('toJSON', function() { - var emptyObjectJSON = '{"type":"object","originX":"center","originY":"center","left":0,"top":0,"width":0,"height":0,"fill":"rgb(0,0,0)",'+ + var emptyObjectJSON = '{"type":"object","originX":"left","originY":"top","left":0,"top":0,"width":0,"height":0,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":""}'; - var augmentedJSON = '{"type":"object","originX":"center","originY":"center","left":0,"top":0,"width":122,"height":0,"fill":"rgb(0,0,0)",'+ + var augmentedJSON = '{"type":"object","originX":"left","originY":"top","left":0,"top":0,"width":122,"height":0,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":[5,2],"strokeLineCap":"round","strokeLineJoin":"bevil","strokeMiterLimit":5,'+ '"scaleX":1.3,"scaleY":1,"angle":0,"flipX":false,"flipY":true,"opacity":0.88,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":""}'; @@ -178,9 +178,9 @@ test('toObject', function() { var emptyObjectRepr = { - 'type': "object", - 'originX': 'center', - 'originY': 'center', + 'type': 'object', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 0, @@ -205,9 +205,9 @@ }; var augmentedObjectRepr = { - 'type': "object", - 'originX': 'center', - 'originY': 'center', + 'type': 'object', + 'originX': 'left', + 'originY': 'top', 'left': 10, 'top': 20, 'width': 30, @@ -337,22 +337,22 @@ cObj.set('width', 123).setCoords(); boundingRect = cObj.getBoundingRect(); - equal(boundingRect.left, -61.5); + equal(boundingRect.left, 0); equal(boundingRect.top, 0); equal(boundingRect.width, 123); equal(boundingRect.height, 0); cObj.set('height', 167).setCoords(); boundingRect = cObj.getBoundingRect(); - equal(boundingRect.left, -61.5); - equal(boundingRect.top, -83.5); + equal(boundingRect.left, 0); + equal(boundingRect.top, 0); equal(boundingRect.width, 123); equal(boundingRect.height, 167); cObj.scale(2).setCoords(); boundingRect = cObj.getBoundingRect(); - equal(boundingRect.left, -123); - equal(boundingRect.top, -167); + equal(boundingRect.left, 0); + equal(boundingRect.top, 0); equal(boundingRect.width, 246); equal(boundingRect.height, 334); }); @@ -452,11 +452,11 @@ }); test('setCoords', function() { - var cObj = new fabric.Object({ left: 200, top: 200, width: 100, height: 100 }); + var cObj = new fabric.Object({ left: 150, top: 150, width: 100, height: 100 }); ok(typeof cObj.setCoords == 'function'); equal(cObj.setCoords(), cObj, 'chainable'); - cObj.set('left', 300).set('top', 300); + cObj.set('left', 250).set('top', 250); // coords should still correspond to initial one, even after invoking `set` equal(cObj.oCoords.tl.x, 150); @@ -586,6 +586,33 @@ } }); +test('toDataURL & reference to canvas', function() { + var data = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQA'+ + 'AABkCAYAAABw4pVUAAAA+UlEQVR4nO3RoRHAQBDEsOu/6YR+B2s'+ + 'gIO4Z3919pMwDMCRtHoAhafMADEmbB2BI2jwAQ9LmARiSNg/AkLR5AI'+ + 'akzQMwJG0egCFp8wAMSZsHYEjaPABD0uYBGJI2D8CQtHkAhqTNAzAkbR'+ + '6AIWnzAAxJmwdgSNo8AEPS5gEYkjYPwJC0eQCGpM0DMCRtHoAhafMADEm'+ + 'bB2BI2jwAQ9LmARiSNg/AkLR5AIakzQMwJG0egCFp8wAMSZsHYEjaPABD0'+ + 'uYBGJI2D8CQtHkAhqTNAzAkbR6AIWnzAAxJmwdgSNo8AEPS5gEYkjYPw'+ + 'JC0eQCGpM0DMCRtHsDjB5K06yueJFXJAAAAAElFTkSuQmCC'; + + var cObj = new fabric.Rect({ + width: 100, height: 100, fill: 'red' + }); + canvas.add(cObj); + + if (!fabric.Canvas.supports('toDataURL')) { + alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); + } + else { + var objCanvas = cObj.canvas; + cObj.toDataURL(); + + equal(objCanvas, cObj.canvas); + } +}); + test('hasStateChanged', function() { var cObj = new fabric.Object(); ok(typeof cObj.hasStateChanged == 'function'); @@ -609,7 +636,7 @@ }); test('intersectsWithRectangle', function() { - var cObj = new fabric.Object({ left: 100, top: 100, width: 100, height: 100 }); + var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); cObj.setCoords(); ok(typeof cObj.intersectsWithRect == 'function'); @@ -623,16 +650,16 @@ }); test('intersectsWithObject', function() { - var cObj = new fabric.Object({ left: 100, top: 100, width: 100, height: 100 }); + var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); cObj.setCoords(); ok(typeof cObj.intersectsWithObject == 'function'); - var cObj2 = new fabric.Object({ left: 50, top: 50, width: 200, height: 200 }); + var cObj2 = new fabric.Object({ left: -150, top: -150, width: 200, height: 200 }); cObj2.setCoords(); ok(cObj.intersectsWithObject(cObj2)); ok(cObj2.intersectsWithObject(cObj)); - var cObj3 = new fabric.Object({ left: 400, top: 356, width: 13, height: 33 }); + var cObj3 = new fabric.Object({ left: 392.5, top: 339.5, width: 13, height: 33 }); cObj3.setCoords(); ok(!cObj.intersectsWithObject(cObj3)); ok(!cObj3.intersectsWithObject(cObj)); @@ -1067,8 +1094,8 @@ }); test('intersectsWithRect', function() { - var object = new fabric.Object({ left: 20, top: 30, width: 40, height: 50, angle: 160 }), - point1 = new fabric.Point(0, 0), + var object = new fabric.Object({ left: 0, top: 0, width: 40, height: 50, angle: 160 }), + point1 = new fabric.Point(-10, -10), point2 = new fabric.Point(20, 30), point3 = new fabric.Point(10, 15), point4 = new fabric.Point(30, 35), @@ -1091,10 +1118,10 @@ object2 = new fabric.Object({ left: 25, top: 35, width: 20, height: 20, angle: 50 }), object3 = new fabric.Object({ left: 50, top: 50, width: 20, height: 20, angle: 0 }); - object.setCoords(); - object1.setCoords(); - object2.setCoords(); - object3.setCoords(); + object.set({ originX: 'center', originY: 'center' }).setCoords(); + object1.set({ originX: 'center', originY: 'center' }).setCoords(); + object2.set({ originX: 'center', originY: 'center' }).setCoords(); + object3.set({ originX: 'center', originY: 'center' }).setCoords(); // object and object1 intersects equal(object.intersectsWithObject(object1), true); @@ -1110,10 +1137,10 @@ object2 = new fabric.Object({ left: 20, top: 30, width: 60, height: 30, angle: 10 }), object3 = new fabric.Object({ left: 50, top: 50, width: 20, height: 20, angle: 0 }); - object.setCoords(); - object1.setCoords(); - object2.setCoords(); - object3.setCoords(); + object.set({ originX: 'center', originY: 'center' }).setCoords(); + object1.set({ originX: 'center', originY: 'center' }).setCoords(); + object2.set({ originX: 'center', originY: 'center' }).setCoords(); + object3.set({ originX: 'center', originY: 'center' }).setCoords(); // object1 is fully contained within object equal(object1.isContainedWithinObject(object), true); @@ -1132,7 +1159,7 @@ point5 = new fabric.Point(80, 80), point6 = new fabric.Point(90, 90); - object.setCoords(); + object.set({ originX: 'center', originY: 'center' }).setCoords(); // area is contained in object (no intersection) equal(object.isContainedWithinRect(point1, point2), true); @@ -1151,7 +1178,7 @@ point5 = new fabric.Point(80, 80), point6 = new fabric.Point(90, 90); - object.setCoords(); + object.set({ originX: 'center', originY: 'center' }).setCoords(); // area is contained in object (no intersection) equal(object.isContainedWithinRect(point1, point2), true); @@ -1169,7 +1196,7 @@ point4 = new fabric.Point(15, 40), point5 = new fabric.Point(30, 15); - object.setCoords(); + object.set({ originX: 'center', originY: 'center' }).setCoords(); // point1 is contained in object equal(object.containsPoint(point1), true); @@ -1183,7 +1210,7 @@ equal(object.containsPoint(point5), false); }); - test('containsPoint width padding', function() { + test('containsPoint with padding', function() { var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160, padding: 5 }), point1 = new fabric.Point(30, 30), point2 = new fabric.Point(10, 20), @@ -1192,7 +1219,7 @@ point5 = new fabric.Point(10, 40), point6 = new fabric.Point(30, 5); - object.setCoords(); + object.set({ originX: 'center', originY: 'center' }).setCoords(); // point1 is contained in object equal(object.containsPoint(point1), true); diff --git a/test/unit/object_interactivity.js b/test/unit/object_interactivity.js new file mode 100644 index 00000000..8a0d67f3 --- /dev/null +++ b/test/unit/object_interactivity.js @@ -0,0 +1,87 @@ +(function() { + + QUnit.module('fabric.ObjectInteractivity'); + + test('isControlVisible', function(){ + ok(fabric.Object); + + var cObj = new fabric.Object({ }); + ok(typeof cObj.isControlVisible == 'function', 'isControlVisible should exist'); + + equal(cObj.isControlVisible('tl'), true); + equal(cObj.isControlVisible('tr'), true); + equal(cObj.isControlVisible('br'), true); + equal(cObj.isControlVisible('bl'), true); + equal(cObj.isControlVisible('ml'), true); + equal(cObj.isControlVisible('mt'), true); + equal(cObj.isControlVisible('mr'), true); + equal(cObj.isControlVisible('mb'), true); + equal(cObj.isControlVisible('mtr'), true); + }); + + test('setControlVisible', function(){ + ok(fabric.Object); + + var cObj = new fabric.Object({ }); + ok(typeof cObj.setControlVisible == 'function', 'setControlVisible should exist'); + equal(cObj.setControlVisible('tl'), cObj, 'chainable'); + + cObj.setControlVisible('tl', false); + equal(cObj.isControlVisible('tl'), false); + cObj.setControlVisible('tl', true); + equal(cObj.isControlVisible('tl'), true); + }); + + test('setControlsVisibility', function(){ + ok(fabric.Object); + + var cObj = new fabric.Object({ }); + ok(typeof cObj.setControlsVisibility == 'function', 'setControlsVisibility should exist'); + equal(cObj.setControlsVisibility(), cObj, 'chainable'); + + cObj.setControlsVisibility({ + bl: false, + br: false, + mb: false, + ml: false, + mr: false, + mt: false, + tl: false, + tr: false, + mtr: false + }); + + equal(cObj.isControlVisible('tl'), false); + equal(cObj.isControlVisible('tr'), false); + equal(cObj.isControlVisible('br'), false); + equal(cObj.isControlVisible('bl'), false); + equal(cObj.isControlVisible('ml'), false); + equal(cObj.isControlVisible('mt'), false); + equal(cObj.isControlVisible('mr'), false); + equal(cObj.isControlVisible('mb'), false); + equal(cObj.isControlVisible('mtr'), false); + + cObj.setControlsVisibility({ + bl: true, + br: true, + mb: true, + ml: true, + mr: true, + mt: true, + tl: true, + tr: true, + mtr: true + }); + + equal(cObj.isControlVisible('tl'), true); + equal(cObj.isControlVisible('tr'), true); + equal(cObj.isControlVisible('br'), true); + equal(cObj.isControlVisible('bl'), true); + equal(cObj.isControlVisible('ml'), true); + equal(cObj.isControlVisible('mt'), true); + equal(cObj.isControlVisible('mr'), true); + equal(cObj.isControlVisible('mb'), true); + equal(cObj.isControlVisible('mtr'), true); + }); + +})(); diff --git a/test/unit/observable.js b/test/unit/observable.js index 4677cd73..bb6f00e4 100644 --- a/test/unit/observable.js +++ b/test/unit/observable.js @@ -88,6 +88,28 @@ test('stopObserving multiple handlers', function() { equal(event2Fired, false); }); +test('stopObserving all events', function() { + var foo = { }; + fabric.util.object.extend(foo, fabric.Observable); + + var eventFired = false, event2Fired = false; + + var handler = function() { + eventFired = true; + }; + var handler2 = function() { + event2Fired = true; + }; + foo.on({'bar:baz': handler, 'blah:blah': handler2}); + + foo.stopObserving(); + + foo.fire('bar:baz'); + equal(eventFired, false); + foo.fire('blah:blah'); + equal(event2Fired, false); +}); + test('observe multiple handlers', function() { var foo = { }; fabric.util.object.extend(foo, fabric.Observable); diff --git a/test/unit/path.js b/test/unit/path.js index fc1c4379..fe7f2d55 100644 --- a/test/unit/path.js +++ b/test/unit/path.js @@ -2,8 +2,8 @@ var REFERENCE_PATH_OBJECT = { 'type': 'path', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 200, 'top': 200, 'width': 200, diff --git a/test/unit/path_group.js b/test/unit/path_group.js index f448b2bd..620aff1c 100644 --- a/test/unit/path_group.js +++ b/test/unit/path_group.js @@ -2,8 +2,8 @@ var REFERENCE_PATH_GROUP_OBJECT = { 'type': 'path-group', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 0, diff --git a/test/unit/polygon.js b/test/unit/polygon.js index e0d9c7ed..8e259e89 100644 --- a/test/unit/polygon.js +++ b/test/unit/polygon.js @@ -9,8 +9,8 @@ var REFERENCE_OBJECT = { 'type': 'polygon', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 10, diff --git a/test/unit/polyline.js b/test/unit/polyline.js index 79ff034f..9239cb9b 100644 --- a/test/unit/polyline.js +++ b/test/unit/polyline.js @@ -9,8 +9,8 @@ var REFERENCE_OBJECT = { 'type': 'polyline', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 10, diff --git a/test/unit/rect.js b/test/unit/rect.js index cfff84a2..c11d6da3 100644 --- a/test/unit/rect.js +++ b/test/unit/rect.js @@ -2,8 +2,8 @@ var REFERENCE_RECT = { 'type': 'rect', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 0, @@ -135,7 +135,7 @@ var rect = new fabric.Rect({ width: 100, height: 100, rx: 20, ry: 30 }); var svg = rect.toSVG(); - equal(svg, ''); + equal(svg, ''); }); test('toObject without default values', function() { diff --git a/test/unit/shadow.js b/test/unit/shadow.js index 3292b8d4..100e767d 100644 --- a/test/unit/shadow.js +++ b/test/unit/shadow.js @@ -135,6 +135,19 @@ equal(JSON.stringify(object), '{"color":"rgb(0,0,0)","blur":0,"offsetX":0,"offsetY":0}'); }); + test('toObject without default value', function() { + var shadow = new fabric.Shadow(); + shadow.includeDefaultValues = false; + + equal(JSON.stringify(shadow.toObject()), '{}'); + + shadow.color = 'red'; + equal(JSON.stringify(shadow.toObject()), '{"color":"red"}'); + + shadow.offsetX = 15; + equal(JSON.stringify(shadow.toObject()), '{"color":"red","offsetX":15}'); + }); + test('toSVG', function() { // reset uid fabric.Object.__uid = 0; @@ -148,4 +161,4 @@ equal(shadow.toSVG(object), ''); }); -})(); \ No newline at end of file +})(); diff --git a/test/unit/text.js b/test/unit/text.js index c12702c3..dbce4820 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -8,8 +8,8 @@ var REFERENCE_TEXT_OBJECT = { 'type': 'text', - 'originX': 'center', - 'originY': 'center', + 'originX': 'left', + 'originY': 'top', 'left': 0, 'top': 0, 'width': 20, @@ -44,7 +44,7 @@ 'useNative': true }; - var TEXT_SVG = 'x'; + var TEXT_SVG = 'x'; test('constructor', function() { ok(fabric.Text); diff --git a/test/unit/util.js b/test/unit/util.js index 913e14f5..0876017b 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -76,6 +76,16 @@ deepEqual(fabric.util.degreesToRadians(), NaN); }); + test('fabric.util.radiansToDegrees', function(){ + ok(typeof fabric.util.radiansToDegrees == 'function'); + + equal(fabric.util.radiansToDegrees(0), 0); + equal(fabric.util.radiansToDegrees(Math.PI / 2), 90); + equal(fabric.util.radiansToDegrees(Math.PI), 180); + + deepEqual(fabric.util.radiansToDegrees(), NaN); + }); + test('fabric.util.getRandomInt', function() { ok(typeof fabric.util.getRandomInt == 'function'); @@ -427,6 +437,26 @@ }, 2000); }); + asyncTest('fabric.util.loadImage with no args', function() { + var callbackInvoked = false; + + if (IMG_URL.indexOf('/home/travis') === 0) { + // image can not be accessed on travis so we're returning early + expect(0); + start(); + return; + } + + fabric.util.loadImage('', function() { + callbackInvoked = true; + }); + + setTimeout(function() { + ok(callbackInvoked, 'callback should be invoked'); + start(); + }, 1000); + }); + var SVG_WITH_1_ELEMENT = '\ \ \ @@ -643,8 +673,18 @@ equal(dan + '', 'My name is Dan Trink and my skills are HTML, CSS, Javascript'); }); - // test('fabric.util.setStyle', function() { - // }); + // element doesn't seem to have style on node + if (!fabric.isLikelyNode) { + test('fabric.util.setStyle', function() { + + ok(typeof fabric.util.setStyle === 'function'); + + var el = fabric.document.createElement('div'); + + fabric.util.setStyle(el, 'color:red'); + equal(el.style.color, 'red'); + }); + } // test('fabric.util.request', function() { // }); @@ -658,6 +698,19 @@ // test('fabric.util.removeListener', function() { // }); + test('fabric.util.drawDashedLine', function() { + ok(typeof fabric.util.drawDashedLine === 'function'); + + var el = fabric.document.createElement('canvas'); + var canvas = fabric.isLikelyNode + ? fabric.createCanvasForNode() + : new fabric.Canvas(el); + + var ctx = canvas.getContext('2d'); + + fabric.util.drawDashedLine(ctx, 0, 0, 100, 100, [5, 5]); + }); + test('fabric.util.array.invoke', function() { ok(typeof fabric.util.array.invoke === 'function');