mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-08 08:21:00 +00:00
Merge branch 'master' of https://github.com/kangax/fabric.js
Conflicts: dist/all.js dist/all.require.js src/brushes/circle_brush.class.js src/brushes/spray_brush.class.js src/canvas.class.js src/mixins/canvas_events.mixin.js src/mixins/object_interactivity.mixin.js src/shapes/group.class.js src/shapes/image.class.js src/shapes/object.class.js src/util/misc.js
This commit is contained in:
commit
ce4809d1e8
101 changed files with 25585 additions and 14237 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
before_commit
|
||||
dist/fabric.js
|
||||
|
|
|
|||
68
.jshintrc
68
.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
|
||||
}
|
||||
"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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
23
HEADER.js
23
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("<!DOCTYPE html><html><head></head><body></body></html>");
|
||||
fabric.document = require("jsdom")
|
||||
.jsdom("<!DOCTYPE html><html><head></head><body></body></html>");
|
||||
|
||||
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"
|
||||
];
|
||||
|
|
|
|||
49
README.md
49
README.md
|
|
@ -1,5 +1,12 @@
|
|||
<a href="http://fabricjs.challengepost.com/?utm_source=partner&utm_medium=banner&utm_campaign=fabricjs" style="display: block">
|
||||
<img src="https://dl.dropboxusercontent.com/u/822184/fabric-js-promo-widget-github.gif" style="width: auto">
|
||||
</a>
|
||||
|
||||
### Fabric
|
||||
[](http://travis-ci.org/#!/kangax/fabric.js)
|
||||
|
||||
[](https://codeclimate.com/repos/526a0ed089af7e6cf2001389/feed)
|
||||
|
||||
<a href="https://npmjs.org/package/fabric"><img src="https://badge.fury.io/js/fabric.png"></a>
|
||||
|
||||
**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).<br>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:
|
||||
|
||||
|
|
|
|||
69
build.js
69
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)');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
12429
dist/all.js
vendored
12429
dist/all.js
vendored
File diff suppressed because it is too large
Load diff
13
dist/all.min.js
vendored
13
dist/all.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/all.min.js.gz
vendored
BIN
dist/all.min.js.gz
vendored
Binary file not shown.
13383
dist/all.require.js
vendored
13383
dist/all.require.js
vendored
File diff suppressed because it is too large
Load diff
3473
lib/event.js
3473
lib/event.js
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -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 <kangax@gmail.com>",
|
||||
"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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
119
src/brushes/|
Normal file
119
src/brushes/|
Normal file
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
67
src/elements_parser.js
Normal file
67
src/elements_parser.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <caption>Sharpen filter</caption>
|
||||
* 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 <caption>Blur filter</caption>
|
||||
* 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 <caption>Emboss filter</caption>
|
||||
* 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 <caption>Emboss filter with opaqueness</caption>
|
||||
* 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<h; y++) {
|
||||
for (var x=0; x<w; x++) {
|
||||
var sy = y;
|
||||
|
|
@ -78,18 +115,23 @@
|
|||
// calculate the weighed sum of the source image pixels that
|
||||
// fall under the convolution matrix
|
||||
var r=0, g=0, b=0, a=0;
|
||||
|
||||
for (var cy=0; cy<side; cy++) {
|
||||
for (var cx=0; cx<side; cx++) {
|
||||
|
||||
var scy = sy + cy - halfSide;
|
||||
var scx = sx + cx - halfSide;
|
||||
if (scy >= 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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <caption>Tint filter with hex color and opacity</caption>
|
||||
* var filter = new fabric.Image.filters.Tint({
|
||||
* color: '#3513B0',
|
||||
* opacity: 0.5
|
||||
* });
|
||||
* object.filters.push(filter);
|
||||
* object.applyFilters(canvas.renderAll.bind(canvas));
|
||||
* @example <caption>Tint filter with rgba color</caption>
|
||||
* 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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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) + '%';
|
||||
|
|
|
|||
17
src/log.js
17
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);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
196
src/mixins/canvas_grouping.mixin.js
Normal file
196
src/mixins/canvas_grouping.mixin.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
114
src/mixins/itext.svg_export.js
Normal file
114
src/mixins/itext.svg_export.js
Normal file
|
|
@ -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 [
|
||||
'<rect fill="', styleDecl.textBackgroundColor,
|
||||
'" transform="translate(',
|
||||
-this.width / 2, ' ',
|
||||
-this.height + heightOfLine, ')',
|
||||
'" x="', lineLeftOffset + charOffset,
|
||||
'" y="', lineTopOffset + heightOfLine,
|
||||
'" width="', charWidth,
|
||||
'" height="', heightOfLine,
|
||||
'"></rect>'
|
||||
].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 [
|
||||
'<tspan x="', lineLeftOffset + charOffset, '" ',
|
||||
yProp, '="', lineTopOffset, '" ',
|
||||
|
||||
(styleDecl.fontFamily ? 'font-family="' + styleDecl.fontFamily.replace(/"/g,'\'') + '" ': ''),
|
||||
(styleDecl.fontSize ? 'font-size="' + styleDecl.fontSize + '" ': ''),
|
||||
(styleDecl.fontStyle ? 'font-style="' + styleDecl.fontStyle + '" ': ''),
|
||||
(styleDecl.fontWeight ? 'font-weight="' + styleDecl.fontWeight + '" ': ''),
|
||||
(styleDecl.textDecoration ? 'text-decoration="' + styleDecl.textDecoration + '" ': ''),
|
||||
'style="', fillStyles, '">',
|
||||
|
||||
fabric.util.string.escapeXml(_char),
|
||||
'</tspan>'
|
||||
].join('');
|
||||
}
|
||||
});
|
||||
/* _TO_SVG_END_ */
|
||||
640
src/mixins/itext_behavior.mixin.js
Normal file
640
src/mixins/itext_behavior.mixin.js
Normal file
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
})();
|
||||
265
src/mixins/itext_click_behavior.mixin.js
Normal file
265
src/mixins/itext_click_behavior.mixin.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
605
src/mixins/itext_key_behavior.mixin.js
Normal file
605
src/mixins/itext_key_behavior.mixin.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
95
src/mixins/object.svg_export.js
Normal file
95
src/mixins/object.svg_export.js
Normal file
|
|
@ -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_ */
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
80
src/mixins/object_stacking.mixin.js
Normal file
80
src/mixins/object_stacking.mixin.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@
|
|||
else if (url) {
|
||||
request(url, 'binary', createImageAndCallBack);
|
||||
}
|
||||
else {
|
||||
callback && callback.call(context, url);
|
||||
}
|
||||
};
|
||||
|
||||
fabric.loadSVGFromURL = function(url, callback, reviver) {
|
||||
|
|
|
|||
790
src/parser.js
790
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(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/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(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/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 = [
|
||||
'<style type="text/css">',
|
||||
'<![CDATA[',
|
||||
markup,
|
||||
']]>',
|
||||
'</style>'
|
||||
].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 = [
|
||||
'<pattern x="0" y="0" id="backgroundColorPattern" ',
|
||||
'width="', canvas.backgroundColor.source.width,
|
||||
'" height="', canvas.backgroundColor.source.height,
|
||||
function _createSVGPattern(markup, canvas, property) {
|
||||
if (canvas[property] && canvas[property].toSVG) {
|
||||
markup.push(
|
||||
'<pattern x="0" y="0" id="', property, 'Pattern" ',
|
||||
'width="', canvas[property].source.width,
|
||||
'" height="', canvas[property].source.height,
|
||||
'" patternUnits="userSpaceOnUse">',
|
||||
'<image x="0" y="0" ',
|
||||
'width="', canvas.backgroundColor.source.width,
|
||||
'" height="', canvas.backgroundColor.source.height,
|
||||
'" xlink:href="', canvas.backgroundColor.source.src,
|
||||
'width="', canvas[property].source.width,
|
||||
'" height="', canvas[property].source.height,
|
||||
'" xlink:href="', canvas[property].source.src,
|
||||
'"></image></pattern>'
|
||||
].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(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/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(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/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 = [
|
||||
'<style type="text/css">',
|
||||
'<![CDATA[',
|
||||
markup,
|
||||
']]>',
|
||||
'</style>'
|
||||
].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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '#<fabric.Group: (' + this.complexity() + ')>';
|
||||
},
|
||||
|
||||
/**
|
||||
* 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_ */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
'"></image>'
|
||||
'" preserveAspectRatio="none"',
|
||||
'></image>'
|
||||
);
|
||||
|
||||
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_ */
|
||||
|
|
|
|||
989
src/shapes/itext.class.js
Normal file
989
src/shapes/itext.class.js
Normal file
|
|
@ -0,0 +1,989 @@
|
|||
(function() {
|
||||
|
||||
var clone = fabric.util.object.clone;
|
||||
|
||||
/**
|
||||
* IText class (introduced in <b>v1.4</b>)
|
||||
* @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
|
||||
*
|
||||
* <p>Supported key combinations:</p>
|
||||
* <pre>
|
||||
* 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
|
||||
* </pre>
|
||||
*
|
||||
* <p>Supported mouse/touch combination</p>
|
||||
* <pre>
|
||||
* Position cursor: click/touch
|
||||
* Create selection: click/touch & drag
|
||||
* Create selection: click & shift + click
|
||||
* Select word: double click
|
||||
* Select line: triple click
|
||||
* </pre>
|
||||
*/
|
||||
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 = [ ];
|
||||
|
||||
})();
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
'<g transform="', this.getSvgTransform(), '">',
|
||||
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(''),
|
||||
'</text>',
|
||||
'</g>'
|
||||
);
|
||||
|
||||
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]),
|
||||
'</tspan>');
|
||||
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(
|
||||
'<tspan x="',
|
||||
lineLeftOffset, '" ',
|
||||
(i === 0 || this.useNative ? 'y' : 'dy'), '="',
|
||||
toFixed(this.useNative
|
||||
? ((lineHeight * i) - this.height / 2)
|
||||
: (lineHeight * lineTopOffsetMultiplier), 2) , '" ',
|
||||
// doing this on <tspan> elements since setting opacity
|
||||
// on containing <text> one doesn't work in Illustrator
|
||||
this._getFillAttributes(this.fill), '>',
|
||||
fabric.util.string.escapeXml(textLine),
|
||||
'</tspan>'
|
||||
);
|
||||
},
|
||||
|
||||
_setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) {
|
||||
textBgRects.push(
|
||||
'<rect ',
|
||||
this._getFillAttributes(this.textBackgroundColor),
|
||||
' x="',
|
||||
toFixed(textLeftOffset + this._boundaries[i].left, 2),
|
||||
'" y="',
|
||||
/* an offset that seems to straighten things out */
|
||||
toFixed((lineHeight * i) - this.height / 2, 2),
|
||||
'" width="',
|
||||
toFixed(this._boundaries[i].width, 2),
|
||||
'" height="',
|
||||
toFixed(this._boundaries[i].height, 2),
|
||||
'"></rect>');
|
||||
},
|
||||
|
||||
_setSVGBg: function(textBgRects) {
|
||||
if (this.backgroundColor && this._boundaries) {
|
||||
textBgRects.push(
|
||||
'<rect ',
|
||||
|
|
@ -906,49 +1000,6 @@
|
|||
toFixed(this.height, 2),
|
||||
'"></rect>');
|
||||
}
|
||||
|
||||
// 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(
|
||||
'<tspan x="',
|
||||
lineLeftOffset, '" ',
|
||||
(i === 0 || this.useNative ? 'y' : 'dy'), '="',
|
||||
toFixed(this.useNative ? ((lineTopOffset * i) - this.height / 2) : (lineTopOffset * lineTopOffsetMultiplier), 2) , '" ',
|
||||
// doing this on <tspan> elements since setting opacity on containing <text> one doesn't work in Illustrator
|
||||
this._getFillAttributes(this.fill), '>',
|
||||
fabric.util.string.escapeXml(textLines[i]),
|
||||
'</tspan>'
|
||||
);
|
||||
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(
|
||||
'<rect ',
|
||||
this._getFillAttributes(this.textBackgroundColor),
|
||||
' x="',
|
||||
toFixed(textLeftOffset + this._boundaries[i].left, 2),
|
||||
'" y="',
|
||||
/* an offset that seems to straighten things out */
|
||||
toFixed((lineTopOffset * i) - this.height / 2, 2),
|
||||
'" width="',
|
||||
toFixed(this._boundaries[i].width, 2),
|
||||
'" height="',
|
||||
toFixed(this._boundaries[i].height, 2),
|
||||
'"></rect>');
|
||||
}
|
||||
return {
|
||||
textSpans: textSpans,
|
||||
textBgRects: textBgRects
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
137
src/util/arc.js
Normal file
137
src/util/arc.js
Normal file
|
|
@ -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<segments; i++) {
|
||||
var th2 = th0 + i * th_arc / segments;
|
||||
var th3 = th0 + (i+1) * th_arc / segments;
|
||||
result[i] = [xc, yc, th2, th3, rx, ry, coords.sin_th, coords.cos_th];
|
||||
}
|
||||
|
||||
arcToSegmentsCache[argsString] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getXYCoords(rotateX, rx, ry, ox, oy, x, y) {
|
||||
|
||||
var th = rotateX * (Math.PI/180);
|
||||
var sin_th = Math.sin(th);
|
||||
var cos_th = Math.cos(th);
|
||||
rx = Math.abs(rx);
|
||||
ry = Math.abs(ry);
|
||||
var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
|
||||
var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
|
||||
var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry);
|
||||
if (pl > 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<segs.length; i++) {
|
||||
var bez = segmentToBezier.apply(this, segs[i]);
|
||||
ctx.bezierCurveTo.apply(ctx, bez);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
(function () {
|
||||
|
||||
var unknown = 'unknown';
|
||||
|
||||
/* EVENT HANDLING */
|
||||
|
||||
function areHostMethods(object) {
|
||||
|
|
@ -174,41 +176,14 @@
|
|||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -216,29 +191,28 @@
|
|||
// 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');
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,47 @@
|
|||
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
|
||||
|
|
@ -100,10 +141,11 @@
|
|||
* @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',
|
||||
|
|
@ -123,14 +165,12 @@
|
|||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +289,7 @@
|
|||
fabric.util.makeElement = makeElement;
|
||||
fabric.util.addClass = addClass;
|
||||
fabric.util.wrapElement = wrapElement;
|
||||
fabric.util.getScrollLeftTop = getScrollLeftTop;
|
||||
fabric.util.getElementOffset = getElementOffset;
|
||||
fabric.util.getElementStyle = getElementStyle;
|
||||
|
||||
|
|
|
|||
|
|
@ -193,25 +193,9 @@
|
|||
* @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;
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
1009
src/util/misc.js
1009
src/util/misc.js
File diff suppressed because it is too large
Load diff
4
test.js
4
test.js
|
|
@ -29,7 +29,9 @@ testrunner.run({
|
|||
'./test/unit/canvas_static.js',
|
||||
'./test/unit/gradient.js',
|
||||
'./test/unit/pattern.js',
|
||||
'./test/unit/shadow.js'
|
||||
'./test/unit/shadow.js',
|
||||
'./test/unit/object_interactivity.js',
|
||||
'./test/unit/itext.js'
|
||||
]
|
||||
}, function(err, report) {
|
||||
if (err) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
var emptyImageCanvasData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAYAAADL1t+KAAAH7ElEQVR4nO3VMQ0AMAzAsPInvYHoMS2yEeTLHADge/M6AADYM3QACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIuMjH4b7osLFBAAAAAElFTkSuQmCC";
|
||||
|
||||
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],'+
|
||||
|
|
@ -19,9 +19,9 @@
|
|||
' -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_OBJ_JSON = '{"type": "path", "originX": "center", "originY": "center", "left": 268, "top": 266, "width": 51, "height": 49,'+
|
||||
var PATH_OBJ_JSON = '{"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],'+
|
||||
|
|
@ -38,15 +38,15 @@
|
|||
'["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]]}';
|
||||
|
||||
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"}';
|
||||
'"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}';
|
||||
|
||||
var el = fabric.document.createElement('canvas');
|
||||
el.width = 600; el.height = 600;
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
canvas.clear();
|
||||
canvas.setActiveGroup(null);
|
||||
canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor;
|
||||
canvas.backgroundImage = '';
|
||||
canvas.overlayColor = fabric.Canvas.prototype.overlayColor;
|
||||
canvas.calcOffset();
|
||||
upperCanvasEl.style.display = 'none';
|
||||
}
|
||||
|
|
@ -207,7 +207,7 @@
|
|||
var rect = makeRect({ left: 102, top: 202 });
|
||||
canvas.add(rect);
|
||||
equal(canvas.centerObjectH(rect), canvas, 'should be chainable');
|
||||
equal(rect.get('left'), upperCanvasEl.width / 2, 'object\'s "left" property should correspond to canvas element\'s center');
|
||||
equal(rect.getCenterPoint().x, upperCanvasEl.width / 2, 'object\'s "left" property should correspond to canvas element\'s center');
|
||||
});
|
||||
|
||||
test('centerObjectV', function() {
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
var rect = makeRect({ left: 102, top: 202 });
|
||||
canvas.add(rect);
|
||||
equal(canvas.centerObjectV(rect), canvas, 'should be chainable');
|
||||
equal(rect.get('top'), upperCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center');
|
||||
equal(rect.getCenterPoint().y, upperCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center');
|
||||
});
|
||||
|
||||
test('centerObject', function() {
|
||||
|
|
@ -224,8 +224,8 @@
|
|||
canvas.add(rect);
|
||||
equal(canvas.centerObject(rect), canvas, 'should be chainable');
|
||||
|
||||
equal(rect.get('top'), upperCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center');
|
||||
equal(rect.get('left'), upperCanvasEl.height / 2, 'object\'s "left" property should correspond to canvas element\'s center');
|
||||
equal(rect.getCenterPoint().y, upperCanvasEl.height / 2, 'object\'s "top" property should correspond to canvas element\'s center');
|
||||
equal(rect.getCenterPoint().x, upperCanvasEl.width / 2, 'object\'s "left" property should correspond to canvas element\'s center');
|
||||
});
|
||||
|
||||
test('straightenObject', function() {
|
||||
|
|
@ -248,19 +248,12 @@
|
|||
ok(typeof canvas.toJSON == 'function');
|
||||
equal(JSON.stringify(canvas.toJSON()), EMPTY_JSON);
|
||||
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 `overlayColor` value should be reflected in json');
|
||||
canvas.add(makeRect());
|
||||
deepEqual(JSON.stringify(canvas.toJSON()), RECT_JSON);
|
||||
});
|
||||
|
||||
test('toDatalessJSON', function() {
|
||||
var path = new fabric.Path('M 100 100 L 300 100 L 200 300 z', {
|
||||
sourcePath: 'http://example.com/'
|
||||
});
|
||||
canvas.add(path);
|
||||
equal(JSON.stringify(canvas.toDatalessJSON()), PATH_DATALESS_JSON);
|
||||
});
|
||||
|
||||
test('toJSON with active group', function() {
|
||||
var rect = new fabric.Rect({ width: 50, height: 50, left: 100, top: 100 });
|
||||
var circle = new fabric.Circle({ radius: 50, left: 50, top: 50 });
|
||||
|
|
@ -273,6 +266,14 @@
|
|||
equal(json, jsonWithActiveGroup);
|
||||
});
|
||||
|
||||
test('toDatalessJSON', function() {
|
||||
var path = new fabric.Path('M 100 100 L 300 100 L 200 300 z', {
|
||||
sourcePath: 'http://example.com/'
|
||||
});
|
||||
canvas.add(path);
|
||||
equal(JSON.stringify(canvas.toDatalessJSON()), PATH_DATALESS_JSON);
|
||||
});
|
||||
|
||||
test('toObject', function() {
|
||||
ok(typeof canvas.toObject == 'function');
|
||||
var expectedObject = {
|
||||
|
|
@ -318,6 +319,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);
|
||||
|
|
@ -345,6 +347,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);
|
||||
|
|
@ -380,6 +383,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);
|
||||
|
|
@ -400,20 +404,37 @@
|
|||
});
|
||||
|
||||
asyncTest('loadFromJSON with no objects', function() {
|
||||
var c1 = new fabric.Canvas('c1', { backgroundColor: 'green' }),
|
||||
c2 = new fabric.Canvas('c2', { backgroundColor: 'red' });
|
||||
var c1 = new fabric.Canvas('c1', { backgroundColor: 'green', overlayColor: 'yellow' }),
|
||||
c2 = new fabric.Canvas('c2', { backgroundColor: 'red', overlayColor: 'orange' });
|
||||
|
||||
var json = c1.toJSON();
|
||||
var fired = false;
|
||||
c2.loadFromJSON(json, function() {
|
||||
fired = true;
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
ok(fired, 'Callback should be fired even if no objects');
|
||||
equal(c2.backgroundColor, 'green', 'Color should be set properly');
|
||||
equal(c2.overlayColor, 'yellow', 'Color should be set properly');
|
||||
start();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
asyncTest('loadFromJSON with empty fabric.Group', function() {
|
||||
var c1 = new fabric.Canvas('c1'),
|
||||
c2 = new fabric.Canvas('c2'),
|
||||
group = new fabric.Group();
|
||||
|
||||
c1.add(group);
|
||||
ok(!c1.isEmpty(), 'canvas is not empty');
|
||||
|
||||
var json = c1.toJSON();
|
||||
var fired = false;
|
||||
c2.loadFromJSON(json, function() {
|
||||
fired = true;
|
||||
|
||||
ok(fired, 'Callback should be fired even if empty fabric.Group exists');
|
||||
start();
|
||||
});
|
||||
});
|
||||
|
||||
asyncTest('loadFromJSON with async content', function() {
|
||||
|
|
@ -860,7 +881,7 @@
|
|||
test('containsPoint', function() {
|
||||
ok(typeof canvas.containsPoint == 'function');
|
||||
|
||||
var rect = new fabric.Rect({ left: 100, top: 100, width: 50, height: 50 });
|
||||
var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50 });
|
||||
canvas.add(rect);
|
||||
|
||||
var canvasEl = canvas.getElement(),
|
||||
|
|
@ -881,8 +902,8 @@
|
|||
};
|
||||
ok(!canvas.containsPoint(eventStub, rect), 'point at (200, 200) should NOT be within area (75, 75, 125, 125)');
|
||||
|
||||
rect.set('left', 200).set('top', 200).setCoords();
|
||||
ok(canvas.containsPoint(eventStub, rect), 'point at (200, 200) should be within area (175, 175, 225, 225)');
|
||||
rect.set('left', 175).set('top', 175).setCoords();
|
||||
ok(canvas.containsPoint(eventStub, rect), 'on rect at (200, 200) should be within area (175, 175, 225, 225)');
|
||||
});
|
||||
|
||||
// asyncTest('resizeImageToFit', function() {
|
||||
|
|
@ -965,8 +986,7 @@
|
|||
canvas.setActiveObject(canvas.item(0));
|
||||
canvas.remove(canvas.item(0));
|
||||
|
||||
// TODO: find out why this is failing
|
||||
// equal(isFired, true, 'removing active object should fire "selection:cleared"');
|
||||
equal(isFired, true, 'removing active object should fire "selection:cleared"');
|
||||
});
|
||||
|
||||
test('clipTo', function() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
var CANVAS_SVG_VIEWBOX = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n'+
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="300" height="300" viewBox="100 100 300 300" xml:space="preserve"><desc>Created with Fabric.js ' + fabric.version + '</desc><defs></defs></svg>';
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)');
|
||||
});
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = '<g transform="translate(80 117.5)"><rect x="-15" y="-5" rx="0" ry="0" width="30" height="10" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(20 -17.5)"/><rect x="-5" y="-20" rx="0" ry="0" width="10" height="40" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(-30 2.5)"/></g>';
|
||||
var expectedSVG = '<g transform="translate(130 160)"><rect x="-15" y="-5" rx="0" ry="0" width="30" height="10" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(25 -25)"/><rect x="-5" y="-20" rx="0" ry="0" width="10" height="40" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(-35 10)"/></g>';
|
||||
equal(group.toSVG(), expectedSVG);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
469
test/unit/itext.js
Normal file
469
test/unit/itext.js
Normal file
|
|
@ -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(), '<g transform="translate(124.38 52)"><text font-family="Times New Roman" font-size="40" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(-124.38 39)"><tspan x="0" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: #112233; opacity: 1;">t</tspan><tspan x="11.11328125" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">e</tspan><tspan x="28.8671875" y="0" style="stroke: #223344; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">s</tspan><tspan x="44.43359375" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">t</tspan><tspan x="55.546875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;"> </tspan><tspan x="65.546875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">f</tspan><tspan x="78.8671875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">o</tspan><tspan x="98.8671875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">o</tspan><tspan x="118.8671875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;"> </tspan><tspan x="128.8671875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">b</tspan><tspan x="148.8671875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">a</tspan><tspan x="166.62109375" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">r</tspan><tspan x="179.94140625" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">-</tspan><tspan x="193.26171875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">b</tspan><tspan x="213.26171875" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">a</tspan><tspan x="231.015625" y="0" style="stroke: none; stroke-width: 0; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); opacity: 1;">z</tspan><tspan x="0" y="0" fill="rgb(0,0,0)">qux</tspan></text></g>');
|
||||
}
|
||||
|
||||
// TODO: more SVG tests here?
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
var LINE_OBJECT = {
|
||||
'type': 'line',
|
||||
'originX': 'center',
|
||||
'originY': 'center',
|
||||
'originX': 'left',
|
||||
'originY': 'top',
|
||||
'left': 12,
|
||||
'top': 13,
|
||||
'width': 2,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
87
test/unit/object_interactivity.js
Normal file
87
test/unit/object_interactivity.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
var REFERENCE_PATH_OBJECT = {
|
||||
'type': 'path',
|
||||
'originX': 'center',
|
||||
'originY': 'center',
|
||||
'originX': 'left',
|
||||
'originY': 'top',
|
||||
'left': 200,
|
||||
'top': 200,
|
||||
'width': 200,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
var REFERENCE_OBJECT = {
|
||||
'type': 'polygon',
|
||||
'originX': 'center',
|
||||
'originY': 'center',
|
||||
'originX': 'left',
|
||||
'originY': 'top',
|
||||
'left': 0,
|
||||
'top': 0,
|
||||
'width': 10,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
var REFERENCE_OBJECT = {
|
||||
'type': 'polyline',
|
||||
'originX': 'center',
|
||||
'originY': 'center',
|
||||
'originX': 'left',
|
||||
'originY': 'top',
|
||||
'left': 0,
|
||||
'top': 0,
|
||||
'width': 10,
|
||||
|
|
|
|||
|
|
@ -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, '<rect x="-50" y="-50" rx="20" ry="30" width="100" height="100" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(0 0)"/>');
|
||||
equal(svg, '<rect x="-50" y="-50" rx="20" ry="30" width="100" height="100" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(50 50)"/>');
|
||||
});
|
||||
|
||||
test('toObject without default values', function() {
|
||||
|
|
|
|||
|
|
@ -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), '<filter id="SVGID_0" y="-40%" height="180%"><feGaussianBlur in="SourceAlpha" stdDeviation="0.6666666666666666"></feGaussianBlur><feOffset dx="10" dy="-10"></feOffset><feMerge><feMergeNode></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter>');
|
||||
});
|
||||
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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 = '<g transform="translate(0 0)"><text font-family="Times New Roman" font-size="40" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(-10 39)"><tspan x="0" y="-26" fill="rgb(0,0,0)">x</tspan></text></g>';
|
||||
var TEXT_SVG = '<g transform="translate(10 26)"><text font-family="Times New Roman" font-size="40" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); opacity: 1;" transform="translate(-10 39)"><tspan x="0" y="-26" fill="rgb(0,0,0)">x</tspan></text></g>';
|
||||
|
||||
test('constructor', function() {
|
||||
ok(fabric.Text);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue