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:
Tom French 2013-12-05 19:45:39 +00:00
commit ce4809d1e8
101 changed files with 25585 additions and 14237 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules
.DS_Store
before_commit
dist/fabric.js

View file

@ -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
}

View file

@ -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

View file

@ -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"
];

View file

@ -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
[![Build Status](https://secure.travis-ci.org/kangax/fabric.js.png?branch=master)](http://travis-ci.org/#!/kangax/fabric.js)
[![Code Climate](https://codeclimate.com/repos/526a0ed089af7e6cf2001389/badges/d1c922dd1511ffa8a72f/gpa.png)](https://codeclimate.com/repos/526a0ed089af7e6cf2001389/feed)
<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:

View file

@ -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)');
});
});
});
});
}

View file

@ -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": {},

View file

@ -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

File diff suppressed because it is too large Load diff

13
dist/all.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/all.min.js.gz vendored

Binary file not shown.

13383
dist/all.require.js vendored

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -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"
},

View file

@ -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 });
}

View file

@ -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);

View file

@ -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();

View file

@ -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
View 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;
}
});

View file

@ -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
View 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);
}
}
};

View file

@ -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 */ {

View file

@ -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;

View file

@ -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
*/

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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;
});
};
/**

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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) + '%';

View file

@ -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);
};
}
});
}

View file

@ -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);

View file

@ -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];
}
});
})();

View file

@ -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

View 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;
}
});
})();

View file

@ -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);

View file

@ -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

View 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_ */

View 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');
}
});
})();

View 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;
}
});

View 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);
}
}
}
});

View 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_ */

View file

@ -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,

View file

@ -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;
}
});
})();

View file

@ -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);

View 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;
}
});

View file

@ -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]);
}

View file

@ -87,6 +87,9 @@
else if (url) {
request(url, 'binary', createImageAndCallBack);
}
else {
callback && callback.call(context, url);
}
};
fabric.loadSVGFromURL = function(url, callback, reviver) {

View file

@ -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);

View file

@ -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);
}
});

View file

@ -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;
}
});

View file

@ -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 */ {

View file

@ -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 */ {

View file

@ -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_ */

View file

@ -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
View 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 = [ ];
})();

View file

@ -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);
}

View file

@ -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
};
}
});

View file

@ -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 };
}
});

View file

@ -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 */ {

View file

@ -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);
};

View file

@ -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);
};

View file

@ -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;

View file

@ -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
};
},
/**

View file

@ -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

View file

@ -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,

View file

@ -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
View 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);
}
};
})();

View file

@ -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');
};
}

View file

@ -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;

View file

@ -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];
}
}

View file

@ -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());
}
/**

File diff suppressed because it is too large Load diff

View file

@ -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) {

View file

@ -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() {

View file

@ -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();
});
});
})();

View file

@ -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,

View file

@ -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)');
});
})();
})();

View file

@ -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,

View file

@ -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);
});

View file

@ -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');

View file

@ -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
View 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?
});
})();

View file

@ -2,8 +2,8 @@
var LINE_OBJECT = {
'type': 'line',
'originX': 'center',
'originY': 'center',
'originX': 'left',
'originY': 'top',
'left': 12,
'top': 13,
'width': 2,

View file

@ -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);

View 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);
});
})();

View file

@ -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);

View file

@ -2,8 +2,8 @@
var REFERENCE_PATH_OBJECT = {
'type': 'path',
'originX': 'center',
'originY': 'center',
'originX': 'left',
'originY': 'top',
'left': 200,
'top': 200,
'width': 200,

View file

@ -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,

View file

@ -9,8 +9,8 @@
var REFERENCE_OBJECT = {
'type': 'polygon',
'originX': 'center',
'originY': 'center',
'originX': 'left',
'originY': 'top',
'left': 0,
'top': 0,
'width': 10,

View file

@ -9,8 +9,8 @@
var REFERENCE_OBJECT = {
'type': 'polyline',
'originX': 'center',
'originY': 'center',
'originX': 'left',
'originY': 'top',
'left': 0,
'top': 0,
'width': 10,

View file

@ -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() {

View file

@ -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>');
});
})();
})();

View file

@ -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