Merge master

This commit is contained in:
kangax 2013-02-06 23:28:17 +01:00
commit b55c35f865
61 changed files with 4363 additions and 3250 deletions

View file

@ -1,6 +1,7 @@
src/
lib/
dist/all.min.js
dist/all.min.js.gz
.DS_Store
HEADER.js
build_docs.js
build.js

View file

@ -2,6 +2,7 @@ language: node_js
node_js:
- 0.6
- 0.8
- 0.9
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq libgif-dev libpng-dev libjpeg8-dev libpango1.0-dev libcairo2-dev

View file

@ -1,6 +1,6 @@
/*! Fabric.js Copyright 2008-2012, Printio (Juriy Zaytsev, Maxim Chernyak) */
/*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */
var fabric = fabric || { version: "1.0.0" };
var fabric = fabric || { version: "1.0.6" };
if (typeof exports !== 'undefined') {
exports.fabric = fabric;

View file

@ -1,4 +1,4 @@
Copyright (c) 2008-2012 Printio (Juriy Zaytsev, Maxim Chernyak)
Copyright (c) 2008-2013 Printio (Juriy Zaytsev, Maxim Chernyak)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -6,12 +6,12 @@
Using Fabric.js, you can create and populate objects on canvas; objects like simple geometrical shapes — rectangles, circles, ellipses, polygons, or more complex shapes consisting of hundreds or thousands of simple paths. You can then scale, move, and rotate these objects with the mouse; modify their properties — color, transparency, z-index, etc. You can also manipulate these objects altogether — grouping them with a simple mouse selection.
Contributions are very much welcome!
[Contributions](https://github.com/kangax/fabric.js/wiki/Love-Fabric%3F-Help-us-by...) are very much welcome!
### Goals
- Unit tested (1400+ tests at the moment)
- Modular (~20 small "classes" and modules)
- Modular (~40 small "classes", modules, mixins)
- Cross-browser
- [Fast](https://github.com/kangax/fabric.js/wiki/Focus-on-speed)
- Encapsulated in one object
@ -89,7 +89,7 @@ Fabric.js started as a foundation for design editor on [printio.ru](http://print
Documentation is always available at [http://fabricjs.com/docs/](http://fabricjs.com/docs/). You can also build it locally, following step 4 from the "Building" section of this README.
Also see [presentation from BK.js](http://www.slideshare.net/kangax/fabricjs-building-acanvaslibrarybk) and [presentation from Falsy Values](http://www.slideshare.net/kangax/fabric-falsy-values-8067834) for an overview of fabric.js, how it works, and its features.
Also see [official 4-part intro series](http://fabricjs.com/articles), [presentation from BK.js](http://www.slideshare.net/kangax/fabricjs-building-acanvaslibrarybk) and [presentation from Falsy Values](http://www.slideshare.net/kangax/fabric-falsy-values-8067834) for an overview of fabric.js, how it works, and its features.
### Optional modules
@ -103,6 +103,7 @@ These are the optional modules that could be specified for inclusion, when build
- **easing** - Adds support for animation easing functions
- **node** — Adds support for running fabric under node.js, with help of [jsdom](https://github.com/tmpvar/jsdom) and [node-canvas](https://github.com/learnboost/node-canvas) libraries.
- **freedrawing** - Adds support for free drawing
- **gestures** - Adds support for multitouch gestures
### Examples of use
@ -131,11 +132,11 @@ Follow [@fabric.js](http://twitter.com/fabricjs) or [@kangax](http://twitter.com
- Ernest Delgado for the original idea of [manipulating images on canvas](http://www.ernestdelgado.com/archive/canvas/).
- [Maxim "hakunin" Chernyak](http://twitter.com/hakunin) for ideas, and help with various parts of the library throughout its life.
- [Sergey Nisnevich](http://nisnya.com) for help with geometry logic.
- Github contributors: @Kingsquare, @cleercode, @jarek-itmore, @sunrei, @khronnuz, @ollym, @Kienz, @willmcneilly, @davidjrice
- Github contributors: @Kingsquare, @cleercode, @jarek-itmore, @sunrei, @khronnuz, @ollym, @Kienz, @garg, @sjpemberton09, @willmcneilly, @davidjrice, @coulix, and [more](https://github.com/kangax/fabric.js/graphs/contributors)
### MIT License
Copyright (c) 2008-2012 Printio (Juriy Zaytsev, Maxim Chernyak)
Copyright (c) 2008-2013 Printio (Juriy Zaytsev, Maxim Chernyak)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -18,13 +18,13 @@ var minifier = buildArgsAsObject.minifier || 'uglifyjs';
var mininfierCmd;
if (minifier === 'yui') {
mininfierCmd = 'java -jar lib/yuicompressor-2.4.2.jar dist/all.js -o dist/all.min.js';
mininfierCmd = 'java -jar lib/yuicompressor-2.4.6.jar dist/all.js -o dist/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';
}
else if (minifier === 'uglifyjs') {
mininfierCmd = 'uglifyjs -o dist/all.min.js dist/all.js';
mininfierCmd = 'uglifyjs --output dist/all.min.js dist/all.js';
}
var includeAllModules = modulesToInclude.length === 1 && modulesToInclude[0] === 'ALL';
@ -91,7 +91,7 @@ var filesToInclude = [
ifSpecifiedInclude('gestures', 'lib/event.js'),
'src/log.js',
'src/observable.js',
'src/observable.mixin.js',
'src/util/misc.js',
'src/util/lang_array.js',
@ -109,6 +109,8 @@ var filesToInclude = [
ifSpecifiedInclude('parser', 'src/parser.js'),
'src/gradient.class.js',
'src/pattern.class.js',
'src/shadow.class.js',
'src/point.class.js',
'src/intersection.class.js',
'src/color.class.js',
@ -123,13 +125,19 @@ var filesToInclude = [
ifSpecifiedInclude('freedrawing', 'src/pattern_brush.class.js'),
ifSpecifiedInclude('interaction', 'src/canvas.class.js'),
ifSpecifiedInclude('interaction', 'src/canvas_events.mixin.js'),
'src/canvas.animation.js',
'src/canvas_animation.mixin.js',
ifSpecifiedInclude('serialization', 'src/canvas.serialization.js'),
ifSpecifiedInclude('gestures', 'src/canvas.gestures.js'),
ifSpecifiedInclude('serialization', 'src/canvas_serialization.mixin.js'),
ifSpecifiedInclude('gestures', 'src/canvas_gestures.mixin.js'),
'src/object.class.js',
'src/object_origin.mixin.js',
'src/object_geometry.mixin.js',
ifSpecifiedInclude('interaction', 'src/object_interactivity.mixin.js'),
'src/line.class.js',
'src/circle.class.js',
'src/triangle.class.js',
@ -142,7 +150,7 @@ var filesToInclude = [
'src/group.class.js',
'src/image.class.js',
ifSpecifiedInclude('object_straightening', 'src/object_straightening.js'),
ifSpecifiedInclude('object_straightening', 'src/object_straightening.mixin.js'),
ifSpecifiedInclude('image_filters', 'src/image_filters.js'),

3367
dist/all.js vendored

File diff suppressed because it is too large Load diff

10
dist/all.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,6 @@
function initAligningGuidelines(canvas) {
var ctx = canvas.getSelectionContext(),
canvasHeight = canvas.getHeight(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 1,
@ -57,11 +56,16 @@ function initAligningGuidelines(canvas) {
var activeObject = e.target,
canvasObjects = canvas.getObjects(),
activeObjectLeft = activeObject.get('left'),
activeObjectTop = activeObject.get('top'),
activeObjectHeight = activeObject.getHeight(),
activeObjectWidth = activeObject.getWidth(),
noneInTheRange = true;
activeObjectCenter = activeObject.getCenterPoint(),
activeObjectLeft = activeObjectCenter.x,
activeObjectTop = activeObjectCenter.y,
activeObjectHeight = activeObject.getBoundingRectHeight(),
activeObjectWidth = activeObject.getBoundingRectWidth(),
horizontalInTheRange = false,
verticalInTheRange = false,
transform = canvas._currentTransform;
if (!transform) return;
// It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions,
// but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move
@ -70,14 +74,15 @@ function initAligningGuidelines(canvas) {
if (canvasObjects[i] === activeObject) continue;
var objectLeft = canvasObjects[i].get('left'),
objectTop = canvasObjects[i].get('top'),
objectHeight = canvasObjects[i].getHeight(),
objectWidth = canvasObjects[i].getWidth();
var objectCenter = canvasObjects[i].getCenterPoint(),
objectLeft = objectCenter.x,
objectTop = objectCenter.y,
objectHeight = canvasObjects[i].getBoundingRectHeight(),
objectWidth = canvasObjects[i].getBoundingRectWidth();
// snap by the horizontal center line
if (isInRange(objectLeft, activeObjectLeft)) {
noneInTheRange = false;
verticalInTheRange = true;
verticalLines.push({
x: objectLeft,
y1: (objectTop < activeObjectTop)
@ -87,12 +92,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset)
: (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
});
activeObject.set('left', objectLeft);
activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), transform.originX, transform.originY);
}
// snap by the left edge
if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) {
noneInTheRange = false;
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1: (objectTop < activeObjectTop)
@ -102,12 +107,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset)
: (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
});
activeObject.set('left', objectLeft - objectWidth / 2 + activeObjectWidth / 2);
activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), transform.originX, transform.originY);
}
// snap by the right edge
if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) {
noneInTheRange = false;
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1: (objectTop < activeObjectTop)
@ -117,12 +122,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset)
: (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
});
activeObject.set('left', objectLeft + objectWidth / 2 - activeObjectWidth / 2);
activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), transform.originX, transform.originY);
}
// snap by the vertical center line
if (isInRange(objectTop, activeObjectTop)) {
noneInTheRange = false;
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop,
x1: (objectLeft < activeObjectLeft)
@ -132,12 +137,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset)
: (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
});
activeObject.set('top', objectTop);
activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), transform.originX, transform.originY);
}
// snap by the top edge
if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
noneInTheRange = false;
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1: (objectLeft < activeObjectLeft)
@ -147,12 +152,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset)
: (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
});
activeObject.set('top', objectTop - objectHeight / 2 + activeObjectHeight / 2);
activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), transform.originX, transform.originY);
}
// snap by the bottom edge
if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
noneInTheRange = false;
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1: (objectLeft < activeObjectLeft)
@ -162,13 +167,21 @@ function initAligningGuidelines(canvas) {
? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset)
: (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
});
activeObject.set('top', objectTop + objectHeight / 2 - activeObjectHeight / 2);
activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), transform.originX, transform.originY);
}
}
if (noneInTheRange) {
verticalLines.length = horizontalLines.length = 0;
if (!horizontalInTheRange) {
horizontalLines.length = 0;
}
if (!verticalInTheRange) {
verticalLines.length = 0;
}
});
canvas.on('before:render', function() {
canvas.clearContext(canvas.contextTop);
});
canvas.on('after:render', function() {
@ -184,4 +197,4 @@ function initAligningGuidelines(canvas) {
verticalLines.length = horizontalLines.length = 0;
canvas.renderAll();
});
}
}

View file

@ -48,19 +48,24 @@ function initCenteringGuidelines(canvas) {
isInHorizontalCenter;
canvas.on('object:moving', function(e) {
object = e.target;
var object = e.target,
objectCenter = object.getCenterPoint(),
transform = canvas._currentTransform;
isInVerticalCenter = object.get('left') in canvasWidthCenterMap,
isInHorizontalCenter = object.get('top') in canvasHeightCenterMap;
if (!transform) return;
if (isInHorizontalCenter) {
object.set('top', canvasHeightCenter);
}
if (isInVerticalCenter) {
object.set('left', canvasWidthCenter);
isInVerticalCenter = objectCenter.x in canvasWidthCenterMap,
isInHorizontalCenter = objectCenter.y in canvasHeightCenterMap;
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(new fabric.Point((isInVerticalCenter ? canvasWidthCenter : objectCenter.x), (isInHorizontalCenter ? canvasHeightCenter : objectCenter.y)), transform.originX, transform.originY);
}
});
canvas.on('before:render', function() {
canvas.clearContext(canvas.contextTop);
});
canvas.on('after:render', function() {
if (isInVerticalCenter) {
showVerticalCenterLine();

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.0.0",
"version": "1.0.6",
"author": "Juriy Zaytsev <kangax@gmail.com>",
"keywords": ["canvas", "graphic", "graphics", "SVG", "node-canvas", "parser", "HTML5", "object model"],
"repository": "git://github.com/kangax/fabric.js",
@ -10,19 +10,19 @@
"url": "http://github.com/kangax/fabric.js/raw/master/LICENSE"
}],
"scripts": {
"build": "node build.js modules=ALL exclude=json,cufon",
"build": "node build.js modules=ALL exclude=json,cufon,gestures",
"test": "node test.js"
},
"dependencies": {
"canvas": "~0.13.0",
"canvas": "~1.0.0",
"jsdom": ">=0.2.3",
"xmldom": ">=0.1.7"
},
"devDependencies": {
"qunit": "0.5.x",
"jshint": "0.9.x",
"uglify-js": "1.3.x"
"uglify-js": ">=2.0.0"
},
"engines": { "node": ">=0.4.0 && <0.9.0" },
"engines": { "node": ">=0.4.0 && <1.0.0" },
"main": "./dist/all.js"
}

View file

@ -2,22 +2,8 @@
var extend = fabric.util.object.extend,
getPointer = fabric.util.getPointer,
addListener = fabric.util.addListener,
removeListener = fabric.util.removeListener,
degreesToRadians = fabric.util.degreesToRadians,
radiansToDegrees = fabric.util.radiansToDegrees,
cursorMap = {
'tr': 'ne-resize',
'br': 'se-resize',
'bl': 'sw-resize',
'tl': 'nw-resize',
'ml': 'w-resize',
'mt': 'n-resize',
'mr': 'e-resize',
'mb': 's-resize'
},
sqrt = Math.sqrt,
atan2 = Math.atan2,
abs = Math.abs,
min = Math.min,
@ -178,418 +164,6 @@
this.calcOffset();
},
/**
* Adds mouse listeners to canvas
* @method _initEvents
* @private
* See configuration documentation for more details.
*/
_initEvents: function () {
var _this = this;
this._onMouseDown = function (e) {
_this.__onMouseDown(e);
addListener(fabric.document, 'mouseup', _this._onMouseUp);
fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp);
addListener(fabric.document, 'mousemove', _this._onMouseMove);
fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove);
removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove);
fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove);
};
this._onMouseUp = function (e) {
_this.__onMouseUp(e);
removeListener(fabric.document, 'mouseup', _this._onMouseUp);
fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp);
removeListener(fabric.document, 'mousemove', _this._onMouseMove);
fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove);
addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove);
fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove);
};
this._onMouseMove = function (e) {
e.preventDefault && e.preventDefault();
_this.__onMouseMove(e);
};
this._onResize = function () {
_this.calcOffset();
};
addListener(fabric.window, 'resize', this._onResize);
if (fabric.isTouchSupported) {
addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
if (typeof Event !== 'undefined' && 'add' in Event) {
Event.add(this.upperCanvasEl, 'gesture', function(e, s) {
_this.__onTransformGesture(e, s);
});
}
}
else {
addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
}
},
/**
* Method that defines the actions when mouse is released on canvas.
* The method resets the currentTransform parameters, store the image corner
* position in the image object and render the canvas on top.
* @method __onMouseUp
* @param {Event} e Event object fired on mouseup
*
*/
__onMouseUp: function (e) {
var target;
if (this.isDrawingMode && this._isCurrentlyDrawing) {
this._isCurrentlyDrawing = false;
this.freeDrawingBrush.onMouseUp();
this.fire('mouse:up', { e: e });
return;
}
if (this._currentTransform) {
var transform = this._currentTransform;
target = transform.target;
if (target._scaling) {
target._scaling = false;
}
// determine the new coords everytime the image changes its position
var i = this._objects.length;
while (i--) {
this._objects[i].setCoords();
}
target.isMoving = false;
// 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._adjustPosition(this._currentTransform.target, this._previousOriginX);
this._previousOriginX = null;
}
}
this._currentTransform = null;
if (this._groupSelector) {
// group selection was completed, determine its bounds
this._findSelectedObjects(e);
}
var activeGroup = this.getActiveGroup();
if (activeGroup) {
activeGroup.setObjectsCoords();
activeGroup.set('isMoving', false);
this._setCursor(this.defaultCursor);
}
// clear selection
this._groupSelector = null;
this.renderAll();
this._setCursorFromEvent(e, target);
// fix for FF
this._setCursor('');
var _this = this;
setTimeout(function () {
_this._setCursorFromEvent(e, target);
}, 50);
this.fire('mouse:up', { target: target, e: e });
target && target.fire('mouseup', { e: e });
},
/**
* Method that defines the actions when mouse is clic ked on canvas.
* The method inits the currentTransform parameters and renders all the
* canvas so the current image can be placed on the top canvas and the rest
* in on the container one.
* @method __onMouseDown
* @param e {Event} Event object fired on mousedown
*
*/
__onMouseDown: function (e) {
var pointer;
// accept only left clicks
var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1;
if (!isLeftClick && !fabric.isTouchSupported) return;
if (this.isDrawingMode) {
pointer = this.getPointer(e);
this._isCurrentlyDrawing = true;
this.discardActiveObject().renderAll();
this.freeDrawingBrush.onMouseDown(pointer);
this.fire('mouse:down', { e: e });
return;
}
// ignore if some object is being transformed at this moment
if (this._currentTransform) return;
var target = this.findTarget(e), corner;
pointer = this.getPointer(e);
if (this._shouldClearSelection(e)) {
this._groupSelector = {
ex: pointer.x,
ey: pointer.y,
top: 0,
left: 0
};
this.deactivateAllWithDispatch();
}
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 (this._shouldHandleGroupLogic(e, target)) {
this._handleGroupLogic(e, target);
target = this.getActiveGroup();
}
else {
if (target !== this.getActiveGroup()) {
this.deactivateAll();
}
this.setActiveObject(target, e);
}
this._setupCurrentTransform(e, target);
}
// we must renderAll so that active image is placed on the top canvas
this.renderAll();
this.fire('mouse:down', { target: target, e: e });
target && target.fire('mousedown', { e: e });
// center origin when rotating
if (corner === 'mtr') {
this._previousOriginX = this._currentTransform.target.originX;
this._adjustPosition(this._currentTransform.target, 'center');
this._currentTransform.left = this._currentTransform.target.left;
this._currentTransform.top = this._currentTransform.target.top;
}
},
/**
* @method _shouldHandleGroupLogic
* @param e {Event}
* @param target {fabric.Object}
* @return {Boolean}
*/
_shouldHandleGroupLogic: function(e, target) {
var activeObject = this.getActiveObject();
return e.shiftKey &&
(this.getActiveGroup() || (activeObject && activeObject !== target))
&& this.selection;
},
/**
* Method that defines the actions when mouse is hovering the canvas.
* The currentTransform parameter will definde whether the user is rotating/scaling/translating
* an image or neither of them (only hovering). A group selection is also possible and would cancel
* all any other type of action.
* In case of an image transformation only the top canvas will be rendered.
* @method __onMouseMove
* @param e {Event} Event object fired on mousemove
*
*/
__onMouseMove: function (e) {
var target, pointer;
if (this.isDrawingMode) {
if (this._isCurrentlyDrawing) {
pointer = this.getPointer(e);
this.freeDrawingBrush.onMouseMove(pointer);
}
this.upperCanvasEl.style.cursor = this.freeDrawingCursor;
this.fire('mouse:move', { e: e });
return;
}
var groupSelector = this._groupSelector;
// We initially clicked in an empty area, so we draw a box for multiple selection.
if (groupSelector !== null) {
pointer = getPointer(e);
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) {
// image/text was hovered-out from, we remove its borders
for (var i = this._objects.length; i--; ) {
if (this._objects[i] && !this._objects[i].active) {
this._objects[i].setActive(false);
}
}
style.cursor = this.defaultCursor;
}
else {
// set proper cursor
this._setCursorFromEvent(e, target);
}
}
else {
// object is being transformed (scaled/rotated/moved/etc.)
pointer = getPointer(e);
var x = pointer.x,
y = pointer.y;
this._currentTransform.target.isMoving = true;
var t = this._currentTransform, reset = false;
if (
(t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY')
&&
(
// Switch from a normal resize to center-based
(e.altKey && (t.originX !== 'center' || t.originY !== 'center'))
||
// Switch from center-based resize to normal one
(!e.altKey && t.originX === 'center' && t.originY === 'center')
)
) {
this._resetCurrentTransform(e);
reset = true;
}
if (this._currentTransform.action === 'rotate') {
this._rotateObject(x, y);
this.fire('object:rotating', {
target: this._currentTransform.target
});
this._currentTransform.target.fire('rotating');
}
else if (this._currentTransform.action === 'scale') {
// rotate object only if shift key is not pressed
// and if it is not a group we are transforming
// TODO
/*if (!e.shiftKey) {
this._rotateObject(x, y);
this.fire('object:rotating', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('rotating');
}*/
// if (!this._currentTransform.target.hasRotatingPoint) {
// this._scaleObject(x, y);
// this.fire('object:scaling', {
// target: this._currentTransform.target
// });
// this._currentTransform.target.fire('scaling');
// }
if (e.shiftKey || this.uniScaleTransform) {
this._currentTransform.currentAction = 'scale';
this._scaleObject(x, y);
}
else {
if (!reset && t.currentAction === 'scale') {
// Switch from a normal resize to proportional
this._resetCurrentTransform(e);
}
this._currentTransform.currentAction = 'scaleEqually';
this._scaleObject(x, y, 'equally');
}
this.fire('object:scaling', {
target: this._currentTransform.target,
e: e
});
}
// else if (this._currentTransform.action === 'scale') {
// this._scaleObject(x, y);
// this.fire('object:scaling', {
// target: this._currentTransform.target
// });
// this._currentTransform.target.fire('scaling');
// }
else if (this._currentTransform.action === 'scaleX') {
this._scaleObject(x, y, 'x');
this.fire('object:scaling', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('scaling', { e: e });
}
else if (this._currentTransform.action === 'scaleY') {
this._scaleObject(x, y, 'y');
this.fire('object:scaling', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('scaling', { e: e });
}
else {
this._translateObject(x, y);
this.fire('object:moving', {
target: this._currentTransform.target,
e: e
});
this._setCursor(this.moveCursor);
this._currentTransform.target.fire('moving', { e: e });
}
// only commit here. when we are actually moving the pictures
this.renderAll();
}
this.fire('mouse:move', { target: target, e: e });
target && target.fire('mousemove', { e: e });
},
/**
* Resets the current transform to its original values and chooses the type of resizing based on the event
* @method _resetCurrentTransform
@ -597,6 +171,7 @@
*/
_resetCurrentTransform: function(e) {
var t = this._currentTransform;
t.target.set('scaleX', t.original.scaleX);
t.target.set('scaleY', t.original.scaleY);
t.target.set('left', t.original.left);
@ -752,7 +327,7 @@
_setupCurrentTransform: function (e, target) {
var action = 'drag',
corner,
pointer = getPointer(e);
pointer = getPointer(e, target.canvas.upperCanvasEl);
corner = target._findTargetCorner(e, this._offset);
if (corner) {
@ -818,6 +393,19 @@
this._resetCurrentTransform(e);
},
/**
* @method _shouldHandleGroupLogic
* @param e {Event}
* @param target {fabric.Object}
* @return {Boolean}
*/
_shouldHandleGroupLogic: function(e, target) {
var activeObject = this.getActiveObject();
return e.shiftKey &&
(this.getActiveGroup() || (activeObject && activeObject !== target))
&& this.selection;
},
/**
* @private
* @method _handleGroupLogic
@ -1019,43 +607,6 @@
target.setAngle(0);
},
/**
* Sets the cursor depending on where the canvas is being hovered.
* Note: very buggy in Opera
* @method _setCursorFromEvent
* @param e {Event} Event object
* @param target {Object} Object that the mouse is hovering, if so.
*/
_setCursorFromEvent: function (e, target) {
var s = this.upperCanvasEl.style;
if (!target) {
s.cursor = this.defaultCursor;
return false;
}
else {
var activeGroup = this.getActiveGroup();
// only show proper corner when group selection is not active
var corner = !!target._findTargetCorner
&& (!activeGroup || !activeGroup.contains(target))
&& target._findTargetCorner(e, this._offset);
if (!corner) {
s.cursor = this.hoverCursor;
}
else {
if (corner in cursorMap) {
s.cursor = cursorMap[corner];
} else if (corner === 'mtr' && target.hasRotatingPoint) {
s.cursor = this.rotationCursor;
} else {
s.cursor = this.defaultCursor;
return false;
}
}
}
return true;
},
/**
* @method _drawSelection
* @private
@ -1082,13 +633,17 @@
// selection border
if (this.selectionDashArray.length > 1) {
var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0: aleft);
var py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0: atop);
ctx.beginPath();
this.drawDashedLine(ctx, px, py, px+aleft, py, this.selectionDashArray);
this.drawDashedLine(ctx, px, py+atop-1, px+aleft, py+atop-1, this.selectionDashArray);
this.drawDashedLine(ctx, px, py, px, py+atop, this.selectionDashArray);
this.drawDashedLine(ctx, px+aleft-1, py, px+aleft-1, py+atop, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px, py, px+aleft, py, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px, py+atop-1, px+aleft, py+atop-1, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px, py, px, py+atop, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px+aleft-1, py, px+aleft-1, py+atop, this.selectionDashArray);
ctx.closePath();
ctx.stroke();
}
@ -1102,47 +657,6 @@
}
},
/**
* Draws a dashed line between two points
*
* This method is used to draw dashed line around selection area.
* See <a href="http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas">dotted stroke in canvas</a>
*
* @method drawDashedLine
* @param ctx {Canvas} context
* @param x {Number} start x coordinate
* @param y {Number} start y coordinate
* @param x2 {Number} end x coordinate
* @param y2 {Number} end y coordinate
* @param da {Array} dash array pattern
*/
drawDashedLine: function(ctx, x, y, x2, y2, da) {
var dx = x2 - x,
dy = y2 - y,
len = sqrt(dx*dx + dy*dy),
rot = atan2(dy, dx),
dc = da.length,
di = 0,
draw = true;
ctx.save();
ctx.translate(x, y);
ctx.moveTo(0, 0);
ctx.rotate(rot);
x = 0;
while (len > x) {
x += da[di++ % dc];
if (x > len) {
x = len;
}
ctx[draw ? 'lineTo' : 'moveTo'](x, 0);
draw = !draw;
}
ctx.restore();
},
/**
* @private
* @method _findSelectedObjects
@ -1199,7 +713,8 @@
if (this.controlsAboveOverlay &&
this.lastRenderedObjectWithControlsAboveOverlay &&
this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay)) {
this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) &&
this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)) {
target = this.lastRenderedObjectWithControlsAboveOverlay;
return target;
}
@ -1247,7 +762,7 @@
* @return {Object} object with "x" and "y" number values
*/
getPointer: function (e) {
var pointer = getPointer(e);
var pointer = getPointer(e, this.upperCanvasEl);
return {
x: pointer.x - this._offset.left,
y: pointer.y - this._offset.top
@ -1452,51 +967,6 @@
this.fire('selection:cleared');
}
return this;
},
/**
* @private
* @method _adjustPosition
* @param obj
* @param {String} to One of left, center, right
*/
_adjustPosition: function(obj, to) {
var angle = fabric.util.degreesToRadians(obj.angle);
var hypotHalf = obj.getWidth() / 2;
var xHalf = Math.cos(angle) * hypotHalf;
var yHalf = Math.sin(angle) * hypotHalf;
var hypotFull = obj.getWidth();
var xFull = Math.cos(angle) * hypotFull;
var yFull = Math.sin(angle) * hypotFull;
if (obj.originX === 'center' && to === 'left' ||
obj.originX === 'right' && to === 'center') {
// move half left
obj.left -= xHalf;
obj.top -= yHalf;
}
else if (obj.originX === 'left' && to === 'center' ||
obj.originX === 'center' && to === 'right') {
// move half right
obj.left += xHalf;
obj.top += yHalf;
}
else if (obj.originX === 'left' && to === 'right') {
// move full right
obj.left += xFull;
obj.top += yFull;
}
else if (obj.originX === 'right' && to === 'left') {
// move full left
obj.left -= xFull;
obj.top -= yFull;
}
obj.setCoords();
obj.originX = to;
}
};
@ -1522,4 +992,4 @@
* @constructor
*/
fabric.Element = fabric.Canvas;
})();
})();

View file

@ -1,4 +1,4 @@
fabric.util.object.extend(fabric.StaticCanvas.prototype, {
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ {
/**
* Animation duration (in ms) for fx* methods

475
src/canvas_events.mixin.js Normal file
View file

@ -0,0 +1,475 @@
(function(){
var cursorMap = {
'tr': 'ne-resize',
'br': 'se-resize',
'bl': 'sw-resize',
'tl': 'nw-resize',
'ml': 'w-resize',
'mt': 'n-resize',
'mr': 'e-resize',
'mb': 's-resize'
},
addListener = fabric.util.addListener,
removeListener = fabric.util.removeListener,
getPointer = fabric.util.getPointer;
fabric.util.object.extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ {
/**
* Adds mouse listeners to canvas
* @method _initEvents
* @private
* See configuration documentation for more details.
*/
_initEvents: function () {
var _this = this;
this._onMouseDown = this._onMouseDown.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this._onResize = this._onResize.bind(this);
addListener(fabric.window, 'resize', this._onResize);
if (fabric.isTouchSupported) {
addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
if (typeof Event !== 'undefined' && 'add' in Event) {
Event.add(this.upperCanvasEl, 'gesture', function(e, s) {
_this.__onTransformGesture(e, s);
});
}
}
else {
addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
}
},
/**
* @method _onMouseDown
* @private
*/
_onMouseDown: function (e) {
this.__onMouseDown(e);
!fabric.isTouchSupported && addListener(fabric.document, 'mouseup', this._onMouseUp);
fabric.isTouchSupported && addListener(fabric.document, 'touchend', this._onMouseUp);
!fabric.isTouchSupported && addListener(fabric.document, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && addListener(fabric.document, 'touchmove', this._onMouseMove);
!fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
},
/**
* @method _onMouseUp
* @private
*/
_onMouseUp: function (e) {
this.__onMouseUp(e);
!fabric.isTouchSupported && removeListener(fabric.document, 'mouseup', this._onMouseUp);
fabric.isTouchSupported && removeListener(fabric.document, 'touchend', this._onMouseUp);
!fabric.isTouchSupported && removeListener(fabric.document, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', this._onMouseMove);
!fabric.isTouchSupported && addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
},
/**
* @method _onMouseMove
* @private
*/
_onMouseMove: function (e) {
e.preventDefault && e.preventDefault();
this.__onMouseMove(e);
},
/**
* @method _onResize
* @private
*/
_onResize: function () {
this.calcOffset();
},
/**
* Method that defines the actions when mouse is released on canvas.
* The method resets the currentTransform parameters, store the image corner
* position in the image object and render the canvas on top.
* @method __onMouseUp
* @param {Event} e Event object fired on mouseup
*
*/
__onMouseUp: function (e) {
var target;
if (this.isDrawingMode && this._isCurrentlyDrawing) {
this._isCurrentlyDrawing = false;
this.freeDrawingBrush.onMouseUp();
this.fire('mouse:up', { e: e });
return;
}
if (this._currentTransform) {
var transform = this._currentTransform;
target = transform.target;
if (target._scaling) {
target._scaling = false;
}
// determine the new coords everytime the image changes its position
var i = this._objects.length;
while (i--) {
this._objects[i].setCoords();
}
target.isMoving = false;
// 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._currentTransform.target.adjustPosition(this._previousOriginX);
this._previousOriginX = null;
}
}
this._currentTransform = null;
if (this._groupSelector) {
// group selection was completed, determine its bounds
this._findSelectedObjects(e);
}
var activeGroup = this.getActiveGroup();
if (activeGroup) {
activeGroup.setObjectsCoords();
activeGroup.set('isMoving', false);
this._setCursor(this.defaultCursor);
}
// clear selection
this._groupSelector = null;
this.renderAll();
this._setCursorFromEvent(e, target);
// fix for FF
this._setCursor('');
var _this = this;
setTimeout(function () {
_this._setCursorFromEvent(e, target);
}, 50);
this.fire('mouse:up', { target: target, e: e });
target && target.fire('mouseup', { e: e });
},
/**
* Method that defines the actions when mouse is clic ked on canvas.
* The method inits the currentTransform parameters and renders all the
* canvas so the current image can be placed on the top canvas and the rest
* in on the container one.
* @method __onMouseDown
* @param e {Event} Event object fired on mousedown
*
*/
__onMouseDown: function (e) {
var pointer;
// accept only left clicks
var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1;
if (!isLeftClick && !fabric.isTouchSupported) return;
if (this.isDrawingMode) {
pointer = this.getPointer(e);
this._isCurrentlyDrawing = true;
this.discardActiveObject().renderAll();
this.freeDrawingBrush.onMouseDown(pointer);
this.fire('mouse:down', { e: e });
return;
}
// ignore if some object is being transformed at this moment
if (this._currentTransform) return;
var target = this.findTarget(e), corner;
pointer = this.getPointer(e);
if (this._shouldClearSelection(e)) {
this._groupSelector = {
ex: pointer.x,
ey: pointer.y,
top: 0,
left: 0
};
this.deactivateAllWithDispatch();
}
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 (this._shouldHandleGroupLogic(e, target)) {
this._handleGroupLogic(e, target);
target = this.getActiveGroup();
}
else {
if (target !== this.getActiveGroup()) {
this.deactivateAll();
}
this.setActiveObject(target, e);
}
this._setupCurrentTransform(e, target);
}
// we must renderAll so that active image is placed on the top canvas
this.renderAll();
this.fire('mouse:down', { target: target, e: e });
target && target.fire('mousedown', { e: e });
// center origin when rotating
if (corner === 'mtr') {
this._previousOriginX = this._currentTransform.target.originX;
this._currentTransform.target.adjustPosition('center');
this._currentTransform.left = this._currentTransform.target.left;
this._currentTransform.top = this._currentTransform.target.top;
}
},
/**
* Method that defines the actions when mouse is hovering the canvas.
* The currentTransform parameter will definde whether the user is rotating/scaling/translating
* an image or neither of them (only hovering). A group selection is also possible and would cancel
* all any other type of action.
* In case of an image transformation only the top canvas will be rendered.
* @method __onMouseMove
* @param e {Event} Event object fired on mousemove
*
*/
__onMouseMove: function (e) {
var target, pointer;
if (this.isDrawingMode) {
if (this._isCurrentlyDrawing) {
pointer = this.getPointer(e);
this.freeDrawingBrush.onMouseMove(pointer);
}
this.upperCanvasEl.style.cursor = this.freeDrawingCursor;
this.fire('mouse:move', { e: e });
return;
}
var groupSelector = this._groupSelector;
// We initially clicked in an empty area, so we draw a box for multiple selection.
if (groupSelector !== null) {
pointer = getPointer(e, this.upperCanvasEl);
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) {
// image/text was hovered-out from, we remove its borders
for (var i = this._objects.length; i--; ) {
if (this._objects[i] && !this._objects[i].active) {
this._objects[i].setActive(false);
}
}
style.cursor = this.defaultCursor;
}
else {
// set proper cursor
this._setCursorFromEvent(e, target);
}
}
else {
// object is being transformed (scaled/rotated/moved/etc.)
pointer = getPointer(e, this.upperCanvasEl);
var x = pointer.x,
y = pointer.y;
this._currentTransform.target.isMoving = true;
var t = this._currentTransform, reset = false;
if (
(t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY')
&&
(
// Switch from a normal resize to center-based
(e.altKey && (t.originX !== 'center' || t.originY !== 'center'))
||
// Switch from center-based resize to normal one
(!e.altKey && t.originX === 'center' && t.originY === 'center')
)
) {
this._resetCurrentTransform(e);
reset = true;
}
if (this._currentTransform.action === 'rotate') {
this._rotateObject(x, y);
this.fire('object:rotating', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('rotating');
}
else if (this._currentTransform.action === 'scale') {
// rotate object only if shift key is not pressed
// and if it is not a group we are transforming
// TODO
/*if (!e.shiftKey) {
this._rotateObject(x, y);
this.fire('object:rotating', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('rotating');
}*/
// if (!this._currentTransform.target.hasRotatingPoint) {
// this._scaleObject(x, y);
// this.fire('object:scaling', {
// target: this._currentTransform.target
// });
// this._currentTransform.target.fire('scaling');
// }
if (e.shiftKey || this.uniScaleTransform) {
this._currentTransform.currentAction = 'scale';
this._scaleObject(x, y);
}
else {
if (!reset && t.currentAction === 'scale') {
// Switch from a normal resize to proportional
this._resetCurrentTransform(e);
}
this._currentTransform.currentAction = 'scaleEqually';
this._scaleObject(x, y, 'equally');
}
this.fire('object:scaling', {
target: this._currentTransform.target,
e: e
});
}
// else if (this._currentTransform.action === 'scale') {
// this._scaleObject(x, y);
// this.fire('object:scaling', {
// target: this._currentTransform.target
// });
// this._currentTransform.target.fire('scaling');
// }
else if (this._currentTransform.action === 'scaleX') {
this._scaleObject(x, y, 'x');
this.fire('object:scaling', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('scaling', { e: e });
}
else if (this._currentTransform.action === 'scaleY') {
this._scaleObject(x, y, 'y');
this.fire('object:scaling', {
target: this._currentTransform.target,
e: e
});
this._currentTransform.target.fire('scaling', { e: e });
}
else {
this._translateObject(x, y);
this.fire('object:moving', {
target: this._currentTransform.target,
e: e
});
this._setCursor(this.moveCursor);
this._currentTransform.target.fire('moving', { e: e });
}
// only commit here. when we are actually moving the pictures
this.renderAll();
}
this.fire('mouse:move', { target: target, e: e });
target && target.fire('mousemove', { e: e });
},
/**
* Sets the cursor depending on where the canvas is being hovered.
* Note: very buggy in Opera
* @method _setCursorFromEvent
* @param e {Event} Event object
* @param target {Object} Object that the mouse is hovering, if so.
*/
_setCursorFromEvent: function (e, target) {
var s = this.upperCanvasEl.style;
if (!target) {
s.cursor = this.defaultCursor;
return false;
}
else {
var activeGroup = this.getActiveGroup();
// only show proper corner when group selection is not active
var corner = target._findTargetCorner
&& (!activeGroup || !activeGroup.contains(target))
&& target._findTargetCorner(e, this._offset);
if (!corner) {
s.cursor = this.hoverCursor;
}
else {
if (corner in cursorMap) {
s.cursor = cursorMap[corner];
}
else if (corner === 'mtr' && target.hasRotatingPoint) {
s.cursor = this.rotationCursor;
}
else {
s.cursor = this.defaultCursor;
return false;
}
}
}
return true;
}
});
})();

View file

@ -3,7 +3,7 @@
var degreesToRadians = fabric.util.degreesToRadians,
radiansToDegrees = fabric.util.radiansToDegrees;
fabric.util.object.extend(fabric.Canvas.prototype, {
fabric.util.object.extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ {
/**
* Method that defines actions when an Event.js gesture is detected on an object. Currently only supports

View file

@ -1,4 +1,4 @@
fabric.util.object.extend(fabric.StaticCanvas.prototype, {
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ {
/**
* Populates canvas with data from the specified dataless JSON
@ -13,9 +13,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
*/
loadFromDatalessJSON: function (json, callback) {
if (!json) {
return;
}
if (!json) return;
// serialize if it wasn't already
var serialized = (typeof json === 'string')
@ -26,9 +24,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
this.clear();
// TODO: test this
this.backgroundColor = serialized.background;
this._enlivenDatalessObjects(serialized.objects, callback);
var _this = this;
this._enlivenDatalessObjects(serialized.objects, function() {
_this._setBgOverlayImages(serialized, callback);
});
},
/**
@ -37,6 +36,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
* @param {Function} callback
*/
_enlivenDatalessObjects: function (objects, callback) {
var _this = this,
numLoadedObjects = 0,
numTotalObjects = objects.length;
/** @ignore */
function onObjectLoaded(object, index) {
@ -128,10 +130,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
}
}
var _this = this,
numLoadedObjects = 0,
numTotalObjects = objects.length;
if (numTotalObjects === 0 && callback) {
callback();
}
@ -140,7 +138,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
objects.forEach(loadObject, this);
}
catch(e) {
fabric.log(e.message);
fabric.log(e);
}
},
@ -163,55 +161,72 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
? JSON.parse(json)
: json;
if (!serialized || (serialized && !serialized.objects)) return;
this.clear();
var _this = this;
this._enlivenObjects(serialized.objects, function () {
_this.backgroundColor = serialized.background;
var backgroundImageLoaded, overlayImageLoaded;
if (serialized.backgroundImage) {
_this.setBackgroundImage(serialized.backgroundImage, function() {
_this.backgroundImageOpacity = serialized.backgroundImageOpacity;
_this.backgroundImageStretch = serialized.backgroundImageStretch;
_this.renderAll();
backgroundImageLoaded = true;
callback && overlayImageLoaded && 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;
callback && backgroundImageLoaded && callback();
});
}
else {
overlayImageLoaded = true;
}
if (!serialized.backgroundImage && !serialized.overlayImage) {
callback && callback();
}
_this._setBgOverlayImages(serialized, callback);
});
return this;
},
_setBgOverlayImages: function(serialized, callback) {
var _this = this,
backgroundPatternLoaded,
backgroundImageLoaded,
overlayImageLoaded;
if (serialized.backgroundImage) {
this.setBackgroundImage(serialized.backgroundImage, function() {
_this.backgroundImageOpacity = serialized.backgroundImageOpacity;
_this.backgroundImageStretch = serialized.backgroundImageStretch;
_this.renderAll();
backgroundImageLoaded = true;
callback && overlayImageLoaded && backgroundPatternLoaded && 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;
callback && backgroundImageLoaded && backgroundPatternLoaded && callback();
});
}
else {
overlayImageLoaded = true;
}
if (serialized.background) {
this.setBackgroundColor(serialized.background, function() {
_this.renderAll();
backgroundPatternLoaded = true;
callback && overlayImageLoaded && backgroundImageLoaded && callback();
});
}
else {
backgroundPatternLoaded = true;
}
if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) {
callback && callback();
}
},
/**
* @method _enlivenObjects
* @param {Array} objects

View file

@ -81,6 +81,7 @@
if (this.fill) {
ctx.fill();
}
this._removeShadow(ctx);
if (this.stroke) {
ctx.stroke();
}

View file

@ -101,6 +101,7 @@
if (this.stroke) {
ctx.stroke();
}
this._removeShadow(ctx);
if (this.fill) {
ctx.fill();
}

View file

@ -65,11 +65,11 @@
/**
* Returns an instance of CanvasGradient
* @method toLiveGradient
* @method toLive
* @param ctx
* @return {CanvasGradient}
*/
toLiveGradient: function(ctx) {
toLive: function(ctx) {
var gradient = ctx.createLinearGradient(
this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2);

View file

@ -102,7 +102,11 @@
if (!noTransform) {
this.transform(ctx);
}
this._setShadow(ctx);
this._render(ctx);
this._removeShadow(ctx);
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
@ -186,14 +190,10 @@
var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined',
imgEl = this._originalImage,
canvasEl = fabric.document.createElement('canvas'),
canvasEl = fabric.util.createCanvasElement(),
replacement = isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'),
_this = this;
if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(canvasEl);
}
canvasEl.width = imgEl.width;
canvasEl.height = imgEl.height;

View file

@ -620,7 +620,8 @@ fabric.Image.filters.Convolute = fabric.util.createClass(/** @scope fabric.Image
0, 1, 0,
0, 0, 0 ];
this.tmpCtx = fabric.document.createElement('canvas').getContext('2d');
var canvasEl = fabric.util.createCanvasElement();
this.tmpCtx = canvasEl.getContext('2d');
},
/**

View file

@ -121,6 +121,10 @@
return this.nodeCanvas.createPNGStream();
};
fabric.StaticCanvas.prototype.createJPEGStream = function(opts) {
return this.nodeCanvas.createJPEGStream(opts);
};
var origSetWidth = fabric.StaticCanvas.prototype.setWidth;
fabric.StaticCanvas.prototype.setWidth = function(width) {
origSetWidth.call(this);
@ -141,4 +145,4 @@
fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight;
}
})();
})();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,308 @@
(function() {
var degreesToRadians = fabric.util.degreesToRadians;
fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
/**
* Returns true if object intersects with an area formed by 2 points
* @method intersectsWithRect
* @param {Object} selectionTL
* @param {Object} selectionBR
* @return {Boolean}
*/
intersectsWithRect: function(selectionTL, selectionBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br = new fabric.Point(oCoords.br.x, oCoords.br.y);
var intersection = fabric.Intersection.intersectPolygonRectangle(
[tl, tr, br, bl],
selectionTL,
selectionBR
);
return intersection.status === 'Intersection';
},
/**
* Returns true if object intersects with another object
* @method intersectsWithObject
* @param {Object} other Object to test
* @return {Boolean}
*/
intersectsWithObject: function(other) {
// extracts coords
function getCoords(oCoords) {
return {
tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br: new fabric.Point(oCoords.br.x, oCoords.br.y)
};
}
var thisCoords = getCoords(this.oCoords),
otherCoords = getCoords(other.oCoords);
var intersection = fabric.Intersection.intersectPolygonPolygon(
[thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl],
[otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl]
);
return intersection.status === 'Intersection';
},
/**
* Returns true if object is fully contained within area of another object
* @method isContainedWithinObject
* @param {Object} other Object to test
* @return {Boolean}
*/
isContainedWithinObject: function(other) {
return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br);
},
/**
* Returns true if object is fully contained within area formed by 2 points
* @method isContainedWithinRect
* @param {Object} selectionTL
* @param {Object} selectionBR
* @return {Boolean}
*/
isContainedWithinRect: function(selectionTL, selectionBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y);
return tl.x > selectionTL.x
&& tr.x < selectionBR.x
&& tl.y > selectionTL.y
&& bl.y < selectionBR.y;
},
/**
* Returns width of an object's bounding rectangle
* @deprecated since 1.0.4
* @method getBoundingRectWidth
* @return {Number} width value
*/
getBoundingRectWidth: function() {
return this.getBoundingRect().width;
},
/**
* Returns height of an object's bounding rectangle
* @deprecated since 1.0.4
* @method getBoundingRectHeight
* @return {Number} height value
*/
getBoundingRectHeight: function() {
return this.getBoundingRect().height;
},
/**
* Returns coordinates of object's bounding rectangle (left, top, width, height)
* @method getBoundingRect
* @return {Object} Object with left, top, width, height properties
*/
getBoundingRect: function() {
this.oCoords || this.setCoords();
var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x];
var minX = fabric.util.array.min(xCoords);
var maxX = fabric.util.array.max(xCoords);
var width = Math.abs(minX - maxX);
var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y];
var minY = fabric.util.array.min(yCoords);
var maxY = fabric.util.array.max(yCoords);
var height = Math.abs(minY - maxY);
return {
left: minX,
top: minY,
width: width,
height: height
};
},
/**
* Returns width of an object
* @method getWidth
* @return {Number} width value
*/
getWidth: function() {
return this.width * this.scaleX;
},
/**
* Returns height of an object
* @method getHeight
* @return {Number} height value
*/
getHeight: function() {
return this.height * this.scaleY;
},
/**
* Makes sure the scale is valid and modifies it if necessary
* @private
* @method _constrainScale
* @param {Number} value
* @return {Number}
*/
_constrainScale: function(value) {
if (Math.abs(value) < this.minScaleLimit) {
if (value < 0)
return -this.minScaleLimit;
else
return this.minScaleLimit;
}
return value;
},
/**
* Scales an object (equally by x and y)
* @method scale
* @param value {Number} scale factor
* @return {fabric.Object} thisArg
* @chainable
*/
scale: function(value) {
value = this._constrainScale(value);
if (value < 0) {
this.flipX = !this.flipX;
this.flipY = !this.flipY;
value *= -1;
}
this.scaleX = value;
this.scaleY = value;
this.setCoords();
return this;
},
/**
* Scales an object to a given width, with respect to bounding box (scaling by x/y equally)
* @method scaleToWidth
* @param value {Number} new width value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToWidth: function(value) {
// adjust to bounding rect factor so that rotated shapes would fit as well
var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth();
return this.scale(value / this.width / boundingRectFactor);
},
/**
* Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
* @method scaleToHeight
* @param value {Number} new height value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToHeight: function(value) {
// adjust to bounding rect factor so that rotated shapes would fit as well
var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight();
return this.scale(value / this.height / boundingRectFactor);
},
/**
* Sets corner position coordinates based on current angle, width and height
* @method setCoords
* @return {fabric.Object} thisArg
* @chainable
*/
setCoords: function() {
var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0,
padding = this.padding,
theta = degreesToRadians(this.angle);
this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2;
this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2;
// If width is negative, make postive. Fixes path selection issue
if (this.currentWidth < 0) {
this.currentWidth = Math.abs(this.currentWidth);
}
var _hypotenuse = Math.sqrt(
Math.pow(this.currentWidth / 2, 2) +
Math.pow(this.currentHeight / 2, 2));
var _angle = Math.atan(this.currentHeight / this.currentWidth);
// offset added for rotate and scale actions
var offsetX = Math.cos(_angle + theta) * _hypotenuse,
offsetY = Math.sin(_angle + theta) * _hypotenuse,
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
var coords = this.getCenterPoint();
var tl = {
x: coords.x - offsetX,
y: coords.y - offsetY
};
var tr = {
x: tl.x + (this.currentWidth * cosTh),
y: tl.y + (this.currentWidth * sinTh)
};
var br = {
x: tr.x - (this.currentHeight * sinTh),
y: tr.y + (this.currentHeight * cosTh)
};
var bl = {
x: tl.x - (this.currentHeight * sinTh),
y: tl.y + (this.currentHeight * cosTh)
};
var ml = {
x: tl.x - (this.currentHeight/2 * sinTh),
y: tl.y + (this.currentHeight/2 * cosTh)
};
var mt = {
x: tl.x + (this.currentWidth/2 * cosTh),
y: tl.y + (this.currentWidth/2 * sinTh)
};
var mr = {
x: tr.x - (this.currentHeight/2 * sinTh),
y: tr.y + (this.currentHeight/2 * cosTh)
};
var mb = {
x: bl.x + (this.currentWidth/2 * cosTh),
y: bl.y + (this.currentWidth/2 * sinTh)
};
var mtr = {
x: tl.x + (this.currentWidth/2 * cosTh),
y: tl.y + (this.currentWidth/2 * sinTh)
};
// debugging
// setTimeout(function() {
// canvas.contextTop.fillStyle = 'green';
// canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
// canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
// canvas.contextTop.fillRect(br.x, br.y, 3, 3);
// canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
// canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
// canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
// canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
// canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
// }, 50);
// clockwise
this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr };
// set coordinates of the draggable boxes in the corners used to scale/rotate the image
this._setCornerCoords();
return this;
}
});
})();

View file

@ -0,0 +1,496 @@
(function(){
var getPointer = fabric.util.getPointer,
degreesToRadians = fabric.util.degreesToRadians;
fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
/**
* Determines which one of the four corners has been clicked
* @method _findTargetCorner
* @private
* @param e {Event} event object
* @param offset {Object} canvas offset
* @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
*/
_findTargetCorner: function(e, offset) {
if (!this.hasControls || !this.active) return false;
var pointer = getPointer(e, this.canvas.upperCanvasEl),
ex = pointer.x - offset.left,
ey = pointer.y - offset.top,
xpoints,
lines;
for (var i in this.oCoords) {
if (i === 'mtr' && !this.hasRotatingPoint) {
continue;
}
if (this.get('lockUniScaling') && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) {
continue;
}
lines = this._getImageLines(this.oCoords[i].corner, i);
// debugging
// canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2);
xpoints = this._findCrossPoints(ex, ey, lines);
if (xpoints % 2 === 1 && xpoints !== 0) {
this.__corner = i;
return i;
}
}
return false;
},
/**
* Helper method to determine how many cross points are between the 4 image edges
* and the horizontal line determined by the position of our mouse when clicked on canvas
* @method _findCrossPoints
* @private
* @param ex {Number} x coordinate of the mouse
* @param ey {Number} y coordinate of the mouse
* @param oCoords {Object} Coordinates of the image being evaluated
*/
_findCrossPoints: function(ex, ey, oCoords) {
var b1, b2, a1, a2, xi, yi,
xcount = 0,
iLine;
for (var lineKey in oCoords) {
iLine = oCoords[lineKey];
// optimisation 1: line below dot. no cross
if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
continue;
}
// optimisation 2: line above dot. no cross
if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
continue;
}
// optimisation 3: vertical line case
if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) {
xi = iLine.o.x;
yi = ey;
}
// calculate the intersection point
else {
b1 = 0;
b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x);
a1 = ey-b1*ex;
a2 = iLine.o.y-b2*iLine.o.x;
xi = - (a1-a2)/(b1-b2);
yi = a1+b1*xi;
}
// dont count xi < ex cases
if (xi >= ex) {
xcount += 1;
}
// optimisation 4: specific for square images
if (xcount === 2) {
break;
}
}
return xcount;
},
/**
* Method that returns an object with the image lines in it given the coordinates of the corners
* @method _getImageLines
* @private
* @param oCoords {Object} coordinates of the image corners
*/
_getImageLines: function(oCoords) {
return {
topline: {
o: oCoords.tl,
d: oCoords.tr
},
rightline: {
o: oCoords.tr,
d: oCoords.br
},
bottomline: {
o: oCoords.br,
d: oCoords.bl
},
leftline: {
o: oCoords.bl,
d: oCoords.tl
}
};
},
/**
* Sets the coordinates of the draggable boxes in the corners of
* the image used to scale/rotate it.
* @method _setCornerCoords
* @private
*/
_setCornerCoords: function() {
var coords = this.oCoords,
theta = degreesToRadians(this.angle),
newTheta = degreesToRadians(45 - this.angle),
cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2,
cosHalfOffset = cornerHypotenuse * Math.cos(newTheta),
sinHalfOffset = cornerHypotenuse * Math.sin(newTheta),
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
coords.tl.corner = {
tl: {
x: coords.tl.x - sinHalfOffset,
y: coords.tl.y - cosHalfOffset
},
tr: {
x: coords.tl.x + cosHalfOffset,
y: coords.tl.y - sinHalfOffset
},
bl: {
x: coords.tl.x - cosHalfOffset,
y: coords.tl.y + sinHalfOffset
},
br: {
x: coords.tl.x + sinHalfOffset,
y: coords.tl.y + cosHalfOffset
}
};
coords.tr.corner = {
tl: {
x: coords.tr.x - sinHalfOffset,
y: coords.tr.y - cosHalfOffset
},
tr: {
x: coords.tr.x + cosHalfOffset,
y: coords.tr.y - sinHalfOffset
},
br: {
x: coords.tr.x + sinHalfOffset,
y: coords.tr.y + cosHalfOffset
},
bl: {
x: coords.tr.x - cosHalfOffset,
y: coords.tr.y + sinHalfOffset
}
};
coords.bl.corner = {
tl: {
x: coords.bl.x - sinHalfOffset,
y: coords.bl.y - cosHalfOffset
},
bl: {
x: coords.bl.x - cosHalfOffset,
y: coords.bl.y + sinHalfOffset
},
br: {
x: coords.bl.x + sinHalfOffset,
y: coords.bl.y + cosHalfOffset
},
tr: {
x: coords.bl.x + cosHalfOffset,
y: coords.bl.y - sinHalfOffset
}
};
coords.br.corner = {
tr: {
x: coords.br.x + cosHalfOffset,
y: coords.br.y - sinHalfOffset
},
bl: {
x: coords.br.x - cosHalfOffset,
y: coords.br.y + sinHalfOffset
},
br: {
x: coords.br.x + sinHalfOffset,
y: coords.br.y + cosHalfOffset
},
tl: {
x: coords.br.x - sinHalfOffset,
y: coords.br.y - cosHalfOffset
}
};
coords.ml.corner = {
tl: {
x: coords.ml.x - sinHalfOffset,
y: coords.ml.y - cosHalfOffset
},
tr: {
x: coords.ml.x + cosHalfOffset,
y: coords.ml.y - sinHalfOffset
},
bl: {
x: coords.ml.x - cosHalfOffset,
y: coords.ml.y + sinHalfOffset
},
br: {
x: coords.ml.x + sinHalfOffset,
y: coords.ml.y + cosHalfOffset
}
};
coords.mt.corner = {
tl: {
x: coords.mt.x - sinHalfOffset,
y: coords.mt.y - cosHalfOffset
},
tr: {
x: coords.mt.x + cosHalfOffset,
y: coords.mt.y - sinHalfOffset
},
bl: {
x: coords.mt.x - cosHalfOffset,
y: coords.mt.y + sinHalfOffset
},
br: {
x: coords.mt.x + sinHalfOffset,
y: coords.mt.y + cosHalfOffset
}
};
coords.mr.corner = {
tl: {
x: coords.mr.x - sinHalfOffset,
y: coords.mr.y - cosHalfOffset
},
tr: {
x: coords.mr.x + cosHalfOffset,
y: coords.mr.y - sinHalfOffset
},
bl: {
x: coords.mr.x - cosHalfOffset,
y: coords.mr.y + sinHalfOffset
},
br: {
x: coords.mr.x + sinHalfOffset,
y: coords.mr.y + cosHalfOffset
}
};
coords.mb.corner = {
tl: {
x: coords.mb.x - sinHalfOffset,
y: coords.mb.y - cosHalfOffset
},
tr: {
x: coords.mb.x + cosHalfOffset,
y: coords.mb.y - sinHalfOffset
},
bl: {
x: coords.mb.x - cosHalfOffset,
y: coords.mb.y + sinHalfOffset
},
br: {
x: coords.mb.x + sinHalfOffset,
y: coords.mb.y + cosHalfOffset
}
};
coords.mtr.corner = {
tl: {
x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset)
},
tr: {
x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset)
},
bl: {
x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset)
},
br: {
x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset)
}
};
},
/**
* Draws borders of an object's bounding box.
* Requires public properties: width, height
* Requires public options: padding, borderColor
* @method drawBorders
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawBorders: function(ctx) {
if (!this.hasBorders) return;
var padding = this.padding,
padding2 = padding * 2,
strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0;
ctx.save();
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = this.borderColor;
var scaleX = 1 / this._constrainScale(this.scaleX),
scaleY = 1 / this._constrainScale(this.scaleY);
ctx.lineWidth = 1 / this.borderScaleFactor;
ctx.scale(scaleX, scaleY);
var w = this.getWidth(),
h = this.getHeight();
ctx.strokeRect(
~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper
~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5,
~~(w + padding2 + strokeWidth * this.scaleX),
~~(h + padding2 + strokeWidth * this.scaleY)
);
if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) {
var rotateHeight = (
this.flipY
? h + (strokeWidth * this.scaleY) + (padding * 2)
: -h - (strokeWidth * this.scaleY) - (padding * 2)
) / 2;
ctx.beginPath();
ctx.moveTo(0, rotateHeight);
ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset));
ctx.closePath();
ctx.stroke();
}
ctx.restore();
return this;
},
/**
* Draws corners of an object's bounding box.
* Requires public properties: width, height, scaleX, scaleY
* Requires public options: cornerSize, padding
* @method drawCorners
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawCorners: function(ctx) {
if (!this.hasControls) return;
var size = this.cornerSize,
size2 = size / 2,
strokeWidth2 = this.strokeWidth / 2,
left = -(this.width / 2),
top = -(this.height / 2),
_left,
_top,
sizeX = size / this.scaleX,
sizeY = size / this.scaleY,
paddingX = this.padding / this.scaleX,
paddingY = this.padding / this.scaleY,
scaleOffsetY = size2 / this.scaleY,
scaleOffsetX = size2 / this.scaleX,
scaleOffsetSizeX = (size2 - size) / this.scaleX,
scaleOffsetSizeY = (size2 - size) / this.scaleY,
height = this.height,
width = this.width,
methodName = this.transparentCorners ? 'strokeRect' : 'fillRect',
isVML = typeof G_vmlCanvasManager !== 'undefined';
ctx.save();
ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY);
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
// top-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// top-right
_left = left + width - scaleOffsetX + strokeWidth2 + paddingX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// bottom-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// bottom-right
_left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
if (!this.get('lockUniScaling')) {
// middle-top
_left = left + width/2 - scaleOffsetX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-bottom
_left = left + width/2 - scaleOffsetX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-right
_left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
_top = top + height/2 - scaleOffsetY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top + height/2 - scaleOffsetY;
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
}
// middle-top-rotate
if (this.hasRotatingPoint) {
_left = left + width/2 - scaleOffsetX;
_top = this.flipY ?
(top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY)
: (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY);
isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
}
ctx.restore();
return this;
}
});
})();

207
src/object_origin.mixin.js Normal file
View file

@ -0,0 +1,207 @@
(function() {
var degreesToRadians = fabric.util.degreesToRadians;
fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
/**
* Translates the coordinates from origin to center coordinates (based on the object's dimensions)
* @method translateToCenterPoint
* @param {fabric.Point} point The point which corresponds to the originX and originY params
* @param {string} enum('left', 'center', 'right') Horizontal origin
* @param {string} enum('top', 'center', 'bottom') Vertical origin
* @return {fabric.Point}
*/
translateToCenterPoint: function(point, originX, originY) {
var cx = point.x, cy = point.y;
if ( originX === "left" ) {
cx = point.x + this.getWidth() / 2;
}
else if ( originX === "right" ) {
cx = point.x - this.getWidth() / 2;
}
if ( originY === "top" ) {
cy = point.y + this.getHeight() / 2;
}
else if ( originY === "bottom" ) {
cy = point.y - this.getHeight() / 2;
}
// Apply the reverse rotation to the point (it's already scaled properly)
return fabric.util.rotatePoint(new fabric.Point(cx, cy), point, degreesToRadians(this.angle));
},
/**
* Translates the coordinates from center to origin coordinates (based on the object's dimensions)
* @method translateToOriginPoint
* @param {fabric.Point} point The point which corresponds to center of the object
* @param {string} enum('left', 'center', 'right') Horizontal origin
* @param {string} enum('top', 'center', 'bottom') Vertical origin
* @return {fabric.Point}
*/
translateToOriginPoint: function(center, originX, originY) {
var x = center.x, y = center.y;
// Get the point coordinates
if ( originX === "left" ) {
x = center.x - this.getWidth() / 2;
}
else if ( originX === "right" ) {
x = center.x + this.getWidth() / 2;
}
if ( originY === "top" ) {
y = center.y - this.getHeight() / 2;
}
else if ( originY === "bottom" ) {
y = center.y + this.getHeight() / 2;
}
// Apply the rotation to the point (it's already scaled properly)
return fabric.util.rotatePoint(new fabric.Point(x, y), center, degreesToRadians(this.angle));
},
/**
* Returns the real center coordinates of the object
* @method getCenterPoint
* @return {fabric.Point}
*/
getCenterPoint: function() {
return this.translateToCenterPoint(
new fabric.Point(this.left, this.top), this.originX, this.originY);
},
/**
* Returns the coordinates of the object based on center coordinates
* @method getOriginPoint
* @param {fabric.Point} point The point which corresponds to the originX and originY params
* @return {fabric.Point}
*/
// getOriginPoint: function(center) {
// return this.translateToOriginPoint(center, this.originX, this.originY);
// },
/**
* Returns the coordinates of the object as if it has a different origin
* @method getPointByOrigin
* @param {string} enum('left', 'center', 'right') Horizontal origin
* @param {string} enum('top', 'center', 'bottom') Vertical origin
* @return {fabric.Point}
*/
// getPointByOrigin: function(originX, originY) {
// var center = this.getCenterPoint();
// return this.translateToOriginPoint(center, originX, originY);
// },
/**
* Returns the point in local coordinates
* @method toLocalPoint
* @param {fabric.Point} The point relative to the global coordinate system
* @return {fabric.Point}
*/
toLocalPoint: function(point, originX, originY) {
var center = this.getCenterPoint();
var x, y;
if (originX !== undefined && originY !== undefined) {
if ( originX === "left" ) {
x = center.x - this.getWidth() / 2;
}
else if ( originX === "right" ) {
x = center.x + this.getWidth() / 2;
}
else {
x = center.x;
}
if ( originY === "top" ) {
y = center.y - this.getHeight() / 2;
}
else if ( originY === "bottom" ) {
y = center.y + this.getHeight() / 2;
}
else {
y = center.y;
}
}
else {
x = this.left;
y = this.top;
}
return fabric.util.rotatePoint(new fabric.Point(point.x, point.y), center, -degreesToRadians(this.angle)).subtractEquals(new fabric.Point(x, y));
},
/**
* Returns the point in global coordinates
* @method toGlobalPoint
* @param {fabric.Point} The point relative to the local coordinate system
* @return {fabric.Point}
*/
// toGlobalPoint: function(point) {
// return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top));
// },
/**
* Sets the position of the object taking into consideration the object's origin
* @method setPositionByOrigin
* @param {fabric.Point} point The new position of the object
* @param {string} enum('left', 'center', 'right') Horizontal origin
* @param {string} enum('top', 'center', 'bottom') Vertical origin
* @return {void}
*/
setPositionByOrigin: function(pos, originX, originY) {
var center = this.translateToCenterPoint(pos, originX, originY);
var position = this.translateToOriginPoint(center, this.originX, this.originY);
this.set('left', position.x);
this.set('top', position.y);
},
/**
* @method adjustPosition
* @param {String} to One of left, center, right
*/
adjustPosition: function(to) {
var angle = degreesToRadians(this.angle);
var hypotHalf = this.getWidth() / 2;
var xHalf = Math.cos(angle) * hypotHalf;
var yHalf = Math.sin(angle) * hypotHalf;
var hypotFull = this.getWidth();
var xFull = Math.cos(angle) * hypotFull;
var yFull = Math.sin(angle) * hypotFull;
if (this.originX === 'center' && to === 'left' ||
this.originX === 'right' && to === 'center') {
// move half left
this.left -= xHalf;
this.top -= yHalf;
}
else if (this.originX === 'left' && to === 'center' ||
this.originX === 'center' && to === 'right') {
// move half right
this.left += xHalf;
this.top += yHalf;
}
else if (this.originX === 'left' && to === 'right') {
// move full right
this.left += xFull;
this.top += yFull;
}
else if (this.originX === 'right' && to === 'left') {
// move full left
this.left -= xFull;
this.top -= yFull;
}
this.setCoords();
this.originX = to;
}
});
})();

View file

@ -1,4 +1,4 @@
fabric.util.object.extend(fabric.Object.prototype, {
fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
/**
* @private

View file

@ -365,8 +365,8 @@
checkIfDone();
}
}
catch(e) {
fabric.log(e.message || e);
catch(err) {
fabric.log(err);
}
}
else {

View file

@ -543,21 +543,26 @@
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill.toLiveGradient
? this.fill.toLiveGradient(ctx)
ctx.fillStyle = this.fill.toLive
? this.fill.toLive(ctx)
: this.fill;
}
if (this.stroke) {
ctx.strokeStyle = this.stroke;
ctx.strokeStyle = this.stroke.toLive
? this.stroke.toLive(ctx)
: this.stroke;
}
ctx.beginPath();
this._setShadow(ctx);
this._render(ctx);
if (this.fill) {
ctx.fill();
}
this._removeShadow(ctx);
if (this.stroke) {
ctx.strokeStyle = this.stroke;
ctx.lineWidth = this.strokeWidth;

View file

@ -73,9 +73,13 @@
}
this.transform(ctx);
this._setShadow(ctx);
for (var i = 0, l = this.paths.length; i < l; ++i) {
this.paths[i].render(ctx, true);
}
this._removeShadow(ctx);
if (this.active) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);

69
src/pattern.class.js Normal file
View file

@ -0,0 +1,69 @@
/**
* Pattern class
* @class Pattern
* @memberOf fabric
*/
fabric.Pattern = fabric.util.createClass(/** @scope fabric.Pattern.prototype */ {
/**
* Repeat property of a pattern (one of repeat, repeat-x, repeat-y)
* @property
* @type String
*/
repeat: 'repeat',
/**
* Constructor
* @method initialize
* @param {Object} [options]
* @return {fabric.Pattern} thisArg
*/
initialize: function(options) {
options || (options = { });
if (options.source) {
this.source = typeof options.source === 'string'
? new Function(options.source)
: options.source;
}
if (options.repeat) {
this.repeat = options.repeat;
}
},
/**
* Returns object representation of a pattern
* @method toObject
* @return {Object}
*/
toObject: function() {
var source;
// callback
if (typeof this.source === 'function') {
source = String(this.source)
.match(/function\s+\w*\s*\(.*\)\s+\{([\s\S]*)\}/)[1];
}
// <img> element
else if (typeof this.source.src === 'string') {
source = this.source.src;
}
return {
source: source,
repeat: this.repeat
};
},
/**
* Returns an instance of CanvasPattern
* @method toLive
* @param ctx
* @return {CanvasPattern}
*/
toLive: function(ctx) {
var source = typeof this.source === 'function' ? this.source() : this.source;
return ctx.createPattern(source, this.repeat);
}
});

View file

@ -208,6 +208,20 @@
return path;
},
/**
* Creates fabric.Path object to add on canvas
* @method createPath
* @param {String} pathData Path data
* @return {fabric.Path} path to add on canvas
*/
createPath: function(pathData) {
var path = new fabric.Path(pathData);
path.fill = null;
path.stroke = this.canvas.freeDrawingColor;
path.strokeWidth = this.canvas.freeDrawingLineWidth;
return path;
},
/**
* On mouseup after drawing the path on contextTop canvas
* we use the points captured to create an new fabric path object
@ -219,10 +233,8 @@
var ctx = this.canvas.contextTop;
ctx.closePath();
var path = this._getSVGPathData();
path = path.join('');
if (path === "M 0 0 Q 0 0 0 0 L 0 0") {
var pathData = this._getSVGPathData().join('');
if (pathData === "M 0 0 Q 0 0 0 0 L 0 0") {
// do not create 0 width/height paths, as they are
// rendered inconsistently across browsers
// Firefox 4, for example, renders a dot,
@ -231,27 +243,23 @@
return;
}
var p = new fabric.Path(path);
p.fill = null;
p.stroke = this.color;
p.strokeWidth = this.width;
this.canvas.add(p);
// set path origin coordinates based on our bounding box
var originLeft = this.box.minx + (this.box.maxx - this.box.minx) /2;
var originTop = this.box.miny + (this.box.maxy - this.box.miny) /2;
this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false);
p.set({ left: originLeft, top: originTop });
var path = this.createPath(pathData);
path.set({ left: originLeft, top: originTop });
// does not change position
p.setCoords();
this.canvas.add(path);
path.setCoords();
this.canvas.contextTop && this.canvas.clearContext(this.canvas.contextTop);
this.canvas.renderAll();
// fire event 'path' created
this.canvas.fire('path:created', { path: p });
this.canvas.fire('path:created', { path: path });
}
});
})();
})();

View file

@ -56,6 +56,15 @@
this.width = (maxX - minX) || 1;
this.height = (maxY - minY) || 1;
// var halfWidth = this.width / 2,
// halfHeight = this.height / 2;
// change points to offset polygon into a bounding box
// this.points.forEach(function(p) {
// p.x -= halfWidth;
// p.y -= halfHeight;
// }, this);
this.minX = minX;
this.minY = minY;
},
@ -108,6 +117,7 @@
if (this.fill) {
ctx.fill();
}
this._removeShadow(ctx);
if (this.stroke) {
ctx.closePath();
ctx.stroke();

View file

@ -92,6 +92,7 @@
if (this.fill) {
ctx.fill();
}
this._removeShadow(ctx);
if (this.stroke) {
ctx.stroke();
}

View file

@ -121,6 +121,8 @@
ctx.fill();
}
this._removeShadow(ctx);
if (this.strokeDashArray) {
this._renderDashedStroke(ctx);
}
@ -129,6 +131,67 @@
}
},
/**
* @private
* @method _renderDashedStroke
*/
_renderDashedStroke: function(ctx) {
if (1 & this.strokeDashArray.length /* if odd number of items */) {
/* duplicate items */
this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
}
var i = 0,
x = -this.width/2, y = -this.height/2,
_this = this,
padding = this.padding,
dashedArrayLength = this.strokeDashArray.length;
ctx.save();
ctx.beginPath();
/** @ignore */
function renderSide(xMultiplier, yMultiplier) {
var lineLength = 0,
lengthDiff = 0,
sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2;
while (lineLength < sideLength) {
var lengthOfSubPath = _this.strokeDashArray[i++];
lineLength += lengthOfSubPath;
if (lineLength > sideLength) {
lengthDiff = lineLength - sideLength;
}
// track coords
if (xMultiplier) {
x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0);
}
else {
y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0);
}
ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y);
if (i >= dashedArrayLength) {
i = 0;
}
}
}
renderSide(1, 0);
renderSide(0, 1);
renderSide(-1, 0);
renderSide(0, -1);
ctx.stroke();
ctx.closePath();
ctx.restore();
},
/**
* @method _normalizeLeftTopProperties
* @private

70
src/shadow.class.js Normal file
View file

@ -0,0 +1,70 @@
/**
* Shadow class
* @class Shadow
* @memberOf fabric
*/
fabric.Shadow = fabric.util.createClass(/** @scope fabric.Shadow.prototype */ {
/**
* Shadow color
* @property
* @type String
*/
color: 'rgb(0,0,0)',
/**
* Shadow blur
* @property
* @type Number
*/
blur: 0,
/**
* Shadow horizontal offset
* @property
* @type Number
*/
offsetX: 0,
/**
* Shadow vertical offset
* @property
* @type Number
*/
offsetY: 0,
/**
* Constructor
* @method initialize
* @param [options] Options object with any of color, blur, offsetX, offsetX properties
* @return {fabric.Shadow} thisArg
*/
initialize: function(options) {
for (var prop in options) {
this[prop] = options[prop];
}
},
/**
* Returns object representation of a shadow
* @method toObject
* @return {Object}
*/
toObject: function() {
return {
color: this.color,
blur: this.blur,
offsetX: this.offsetX,
offsetY: this.offsetY
};
},
/**
* Returns SVG representation of a shadow
* @method toSVG
* @return {String}
*/
toSVG: function() {
}
});

View file

@ -148,6 +148,9 @@
if (options.backgroundImage) {
this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this));
}
if (options.backgroundColor) {
this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this));
}
this.calcOffset();
},
@ -211,6 +214,33 @@
return this;
},
/**
* Sets background color for this canvas
* @method setBackgroundColor
* @param {String|fabric.Pattern} Color of pattern to set background color to
* @param {Function} callback callback to invoke when background color is set
* @return {fabric.Canvas} thisArg
* @chainable
*/
setBackgroundColor: function(backgroundColor, callback) {
if (backgroundColor.source) {
var _this = this;
fabric.util.loadImage(backgroundColor.source, function(img) {
_this.backgroundColor = new fabric.Pattern({
source: img,
pattern: backgroundColor.pattern
});
callback && callback();
});
}
else {
this.backgroundColor = backgroundColor;
callback && callback();
}
return this;
},
/**
* @private
* @method _createCanvasElement
@ -232,12 +262,8 @@
* @param {HTMLElement} element
*/
_initCanvasElement: function(element) {
if (typeof element.getContext === 'undefined' &&
typeof G_vmlCanvasManager !== 'undefined' &&
G_vmlCanvasManager.initElement) {
fabric.util.createCanvasElement(element);
G_vmlCanvasManager.initElement(element);
}
if (typeof element.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
}
@ -510,6 +536,7 @@
if (this.contextTop) {
this.clearContext(this.contextTop);
}
this.fire('canvas:cleared');
this.renderAll();
return this;
},
@ -533,12 +560,17 @@
this.clearContext(canvasToDrawOn);
}
this.fire('before:render');
if (this.clipTo) {
this._clipCanvas(canvasToDrawOn);
}
if (this.backgroundColor) {
canvasToDrawOn.fillStyle = this.backgroundColor;
canvasToDrawOn.fillStyle = this.backgroundColor.toLive
? this.backgroundColor.toLive(canvasToDrawOn)
: this.backgroundColor;
canvasToDrawOn.fillRect(0, 0, this.width, this.height);
}
@ -546,8 +578,6 @@
this._drawBackroundImage(canvasToDrawOn);
}
this.fire('before:render');
var activeGroup = this.getActiveGroup();
for (var i = 0, length = this._objects.length; i < length; ++i) {
if (!activeGroup ||
@ -687,6 +717,8 @@
var data = (fabric.StaticCanvas.supports('toDataURLWithQuality'))
? canvasEl.toDataURL('image/' + format, quality)
: canvasEl.toDataURL('image/' + format);
this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
return data;
},
@ -740,6 +772,7 @@
this.setActiveObject(activeObject);
}
this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
return dataURL;
@ -874,7 +907,9 @@
}
return object;
}, this),
background: this.backgroundColor
background: (this.backgroundColor && this.backgroundColor.toObject)
? this.backgroundColor.toObject()
: this.backgroundColor
};
if (this.backgroundImage) {
data.backgroundImage = this.backgroundImage.src;
@ -894,13 +929,22 @@
* Returns SVG representation of canvas
* @function
* @method toSVG
* @param {Object} [options] Options for SVG output ("suppressPreamble: true"
* will start the svg output directly at "<svg...")
* @return {String}
*/
toSVG: function() {
var markup = [
'<?xml version="1.0" standalone="no" ?>',
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" ',
'"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
toSVG: function(options) {
options || (options = { });
var markup = [];
if (!options.suppressPreamble) {
markup.push(
'<?xml version="1.0" standalone="no" ?>',
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" ',
'"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">'
);
}
markup.push(
'<svg ',
'xmlns="http://www.w3.org/2000/svg" ',
'xmlns:xlink="http://www.w3.org/1999/xlink" ',
@ -910,7 +954,7 @@
'xml:space="preserve">',
'<desc>Created with Fabric.js ', fabric.version, '</desc>',
fabric.createSVGFontFacesMarkup(this.getObjects())
];
);
if (this.backgroundImage) {
markup.push(
@ -959,14 +1003,22 @@
* @return {Object} removed object
*/
remove: function (object) {
removeFromArray(this._objects, object);
// removing active object should fire "selection:cleared" events
if (this.getActiveObject() === object) {
// removing active object should fire "selection:cleared" events
this.fire('before:selection:cleared', { target: object });
this.discardActiveObject();
this.fire('selection:cleared');
}
var objects = this._objects;
var index = objects.indexOf(object);
// removing any object should fire "objct:removed" events
if (index !== -1) {
objects.splice(index,1);
this.fire('object:removed', { target: object });
}
this.renderAll();
return object;
},
@ -1191,11 +1243,8 @@
* `null` if canvas element or context can not be initialized
*/
supports: function (methodName) {
var el = fabric.document.createElement('canvas');
var el = fabric.util.createCanvasElement();
if (typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(el);
}
if (!el || !el.getContext) {
return null;
}

View file

@ -20,7 +20,7 @@
fabric.Text = fabric.util.createClass(fabric.Object, /** @scope fabric.Text.prototype */ {
/**
* Font size
* Font size (in pixels)
* @property
* @type Number
*/
@ -147,11 +147,7 @@
* @method _initDimensions
*/
_initDimensions: function() {
var canvasEl = fabric.document.createElement('canvas');
if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(canvasEl);
}
var canvasEl = fabric.util.createCanvasElement();
this._render(canvasEl.getContext('2d'));
},
@ -322,8 +318,8 @@
* @method _setTextStyles
*/
_setTextStyles: function(ctx) {
ctx.fillStyle = this.fill.toLiveGradient
? this.fill.toLiveGradient(ctx)
ctx.fillStyle = this.fill.toLive
? this.fill.toLive(ctx)
: this.fill;
ctx.strokeStyle = this.strokeStyle;
ctx.lineWidth = this.strokeWidth;

View file

@ -172,27 +172,41 @@
* @method getPointer
* @memberOf fabric.util
* @param {Event} event
* @param {HTMLCanvasElement} upperCanvasEl &lt;canvas> element on which object selection is drawn
*/
function getPointer(event) {
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;
while (element && element.parentNode && !firstFixedAncestor) {
element = element.parentNode;
element = element.parentNode;
if (element !== fabric.document && fabric.util.getElementPosition(element) === 'fixed') firstFixedAncestor = element;
if (element !== fabric.document && fabric.util.getElementPosition(element) === 'fixed') firstFixedAncestor = element;
if (element !== fabric.document && orgElement !== upperCanvasEl && fabric.util.getElementPosition(element) === 'absolute') {
scrollLeft = 0;
scrollTop = 0;
}
else if (element === fabric.document && orgElement !== upperCanvasEl) {
scrollLeft = body.scrollLeft || docElement.scrollLeft || 0;
scrollTop = body.scrollTop || docElement.scrollTop || 0;
}
else {
scrollLeft += element.scrollLeft || 0;
scrollTop += element.scrollTop || 0;
}
}
return {
x: pointerX(event) + scrollLeft,
y: pointerY(event) + scrollTop
x: pointerX(event) + scrollLeft,
y: pointerY(event) + scrollTop
};
}
@ -209,10 +223,10 @@
if (fabric.isTouchSupported) {
pointerX = function(event) {
return event.touches && event.touches[0] && event.touches[0].pageX || event.clientX;
return (event.touches && event.touches[0] ? (event.touches[0].pageX - (event.touches[0].pageX - event.touches[0].clientX)) || event.clientX : event.clientX);
};
pointerY = function(event) {
return event.touches && event.touches[0] && event.touches[0].pageY || event.clientY;
return (event.touches && event.touches[0] ? (event.touches[0].pageY - (event.touches[0].pageY - event.touches[0].clientY)) || event.clientY : event.clientY);
};
}

View file

@ -1,5 +1,8 @@
(function() {
var sqrt = Math.sqrt,
atan2 = Math.atan2;
/**
* @namespace
*/
@ -295,6 +298,55 @@
}
}
/**
* Draws a dashed line between two points
*
* This method is used to draw dashed line around selection area.
* See <a href="http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas">dotted stroke in canvas</a>
*
* @method drawDashedLine
* @param ctx {Canvas} context
* @param x {Number} start x coordinate
* @param y {Number} start y coordinate
* @param x2 {Number} end x coordinate
* @param y2 {Number} end y coordinate
* @param da {Array} dash array pattern
*/
function drawDashedLine(ctx, x, y, x2, y2, da) {
var dx = x2 - x,
dy = y2 - y,
len = sqrt(dx*dx + dy*dy),
rot = atan2(dy, dx),
dc = da.length,
di = 0,
draw = true;
ctx.save();
ctx.translate(x, y);
ctx.moveTo(0, 0);
ctx.rotate(rot);
x = 0;
while (len > x) {
x += da[di++ % dc];
if (x > len) {
x = len;
}
ctx[draw ? 'lineTo' : 'moveTo'](x, 0);
draw = !draw;
}
ctx.restore();
}
function createCanvasElement() {
var canvasEl = fabric.document.createElement('canvas');
if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(canvasEl);
}
return canvasEl;
}
fabric.util.removeFromArray = removeFromArray;
fabric.util.degreesToRadians = degreesToRadians;
fabric.util.radiansToDegrees = radiansToDegrees;
@ -308,4 +360,7 @@
fabric.util.enlivenObjects = enlivenObjects;
fabric.util.groupSVGElements = groupSVGElements;
fabric.util.populateWithProperties = populateWithProperties;
fabric.util.drawDashedLine = drawDashedLine;
fabric.util.createCanvasElement = createCanvasElement;
})();

View file

@ -25,7 +25,9 @@ testrunner.run({
'./test/unit/parser.js',
'./test/unit/canvas.js',
'./test/unit/canvas_static.js',
'./test/unit/gradient.js'
'./test/unit/gradient.js',
'./test/unit/pattern.js',
'./test/unit/shadow.js'
]
}, function(err, report) {
if(report.failed > 0){

BIN
test/fixtures/greyfloral.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -23,12 +23,12 @@
var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"center","originY":"center","left":200,"top":200,"width":200,"height":200,"fill":"rgb(0,0,0)",'+
'"overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,'+
'"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,'+
'"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null,'+
'"path":"http://example.com/"}],"background":""}';
var RECT_JSON = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)","overlayFill":null,'+
'"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+
'"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0}],'+
'"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null,"rx":0,"ry":0}],'+
'"background":"#ff5555"}';
var el = fabric.document.createElement('canvas');

View file

@ -21,17 +21,17 @@
var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"center","originY":"center","left":200,"top":200,"width":200,"height":200,"fill":"rgb(0,0,0)",'+
'"overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,'+
'"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,'+
'"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null,'+
'"path":"http://example.com/"}],"background":""}';
var RECT_JSON = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)","overlayFill":null,'+
'"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,'+
'"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0}],'+
'"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null,"rx":0,"ry":0}],'+
'"background":"#ff5555"}';
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)","overlayFill":null,'+
'"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,'+
'"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"padding":123,"foo":"bar","rx":0,"ry":0}],'+
'"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null,"padding":123,"foo":"bar","rx":0,"ry":0}],'+
'"background":""}';
// force creation of static canvas
@ -193,6 +193,15 @@
equal(rect.getAngle(), 90, 'angle should be coerced to 90 (from 100)');
});
test('toSVG without preamble', function() {
ok(typeof canvas.toSVG == 'function');
var withPreamble = canvas.toSVG();
var withoutPreamble = canvas.toSVG({suppressPreamble: true});
ok(withPreamble != withoutPreamble);
equal(withoutPreamble.slice(0, 4), '<svg', 'svg should start with root node when premable is suppressed');
});
test('toJSON', function() {
ok(typeof canvas.toJSON == 'function');
equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":""}');
@ -682,4 +691,4 @@
});
});
})();
})();

View file

@ -84,6 +84,7 @@
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false,
'shadow': null,
'radius': 0
};
ok(typeof circle.toObject == 'function');

View file

@ -47,7 +47,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
ok(typeof ellipse.toObject == 'function');
deepEqual(defaultProperties, ellipse.toObject());

View file

@ -48,10 +48,10 @@
equal(object.colorStops, gradient.colorStops);
});
test('toLiveGradient', function() {
test('toLive', function() {
var gradient = createGradient();
ok(typeof gradient.toLiveGradient == 'function');
ok(typeof gradient.toLive == 'function');
});
test('fromElement', function() {

View file

@ -143,6 +143,7 @@
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false,
'shadow': null,
'angle': 0,
'flipX': false,
'flipY': false,

View file

@ -40,6 +40,7 @@
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false,
'shadow': null,
'filters': []
};

View file

@ -28,7 +28,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
QUnit.module('fabric.Line');

View file

@ -111,11 +111,11 @@
test('toJSON', function() {
var emptyObjectJSON = '{"type":"object","originX":"center","originY":"center","left":0,"top":0,"width":0,"height":0,"fill":"rgb(0,0,0)",'+
'"overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,'+
'"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false}';
'"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null}';
var augmentedJSON = '{"type":"object","originX":"center","originY":"center","left":0,"top":0,"width":122,"height":0,"fill":"rgb(0,0,0)",'+
'"overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1.3,"scaleY":1,"angle":0,'+
'"flipX":false,"flipY":true,"opacity":0.88,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false}';
'"flipX":false,"flipY":true,"opacity":0.88,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":true,"transparentCorners":true,"perPixelTargetFind":false,"shadow":null}';
var cObj = new fabric.Object();
ok(typeof cObj.toJSON == 'function');
@ -150,7 +150,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
var augmentedObjectRepr = {
@ -177,7 +178,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
var cObj = new fabric.Object();
@ -811,4 +813,22 @@
equal(object.toObject().fill.colorStops['1'], 'green');
});
test('setShadow', function() {
var object = new fabric.Object();
object.setShadow({
color: 'red',
blur: 10,
offsetX: 5,
offsetY: 15
});
ok(object.shadow instanceof fabric.Shadow);
equal(object.shadow.color, 'red');
equal(object.shadow.blur, 10);
equal(object.shadow.offsetX, 5);
equal(object.shadow.offsetY, 15);
});
})();

View file

@ -25,7 +25,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
function getPathElement(path) {

View file

@ -1,31 +1,32 @@
(function(){
var REFERENCE_PATH_GROUP_OBJECT = {
'type': 'path-group',
'originX': 'center',
'originY': 'center',
'left': 0,
'top': 0,
'width': 0,
'height': 0,
'fill': '',
'overlayFill': null,
'stroke': null,
'strokeWidth': 1,
'strokeDashArray': null,
'scaleX': 1,
'scaleY': 1,
'angle': 0,
'flipX': false,
'flipY': false,
'opacity': 1,
'selectable': true,
'hasControls': true,
'hasBorders': true,
'hasRotatingPoint': true,
'type': 'path-group',
'originX': 'center',
'originY': 'center',
'left': 0,
'top': 0,
'width': 0,
'height': 0,
'fill': '',
'overlayFill': null,
'stroke': null,
'strokeWidth': 1,
'strokeDashArray': null,
'scaleX': 1,
'scaleY': 1,
'angle': 0,
'flipX': false,
'flipY': false,
'opacity': 1,
'selectable': true,
'hasControls': true,
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false,
'paths': getPathObjects()
'shadow': null,
'paths': getPathObjects()
};
function getPathElement(path) {

77
test/unit/pattern.js Normal file
View file

@ -0,0 +1,77 @@
(function() {
function createImageElement() {
return fabric.isLikelyNode ? new (require('canvas').Image) : fabric.document.createElement('img');
}
function setSrc(img, src, callback) {
if (fabric.isLikelyNode) {
require('fs').readFile(src, function(err, imgData) {
if (err) throw err;
img.src = imgData;
img._src = src;
callback && callback();
});
}
else {
img.src = src;
callback && callback();
}
}
QUnit.module('fabric.Pattern');
var img = createImageElement();
setSrc(img, fabric.isLikelyNode ?
(__dirname + '/../fixtures/greyfloral.png')
: '../fixtures/greyfloral.png');
function createPattern() {
return new fabric.Pattern({
source: img
});
}
test('constructor', function() {
ok(fabric.Pattern);
var pattern = createPattern();
ok(pattern instanceof fabric.Pattern, 'should inherit from fabric.Pattern');
});
test('properties', function() {
var pattern = createPattern();
equal(pattern.source, img);
equal(pattern.repeat, 'repeat');
});
test('toObject', function() {
var pattern = createPattern();
ok(typeof pattern.toObject == 'function');
var object = pattern.toObject();
// node-canvas doesn't give <img> "src"
if (img.src) {
equal(object.source, '../fixtures/greyfloral.png');
}
equal(object.repeat, 'repeat');
var sourceExecuted;
var patternWithGetSource = new fabric.Pattern({
source: function() {return fabric.document.createElement("canvas")}
});
var object2 = patternWithGetSource.toObject();
equal(object2.source, 'return fabric.document.createElement("canvas")');
equal(object2.repeat, 'repeat');
});
test('toLive', function() {
var pattern = createPattern();
ok(typeof pattern.toLive == 'function');
});
})();

View file

@ -32,7 +32,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
QUnit.module('fabric.Polygon');
@ -46,7 +47,7 @@
ok(polygon instanceof fabric.Object);
equal(polygon.type, 'polygon');
deepEqual(getPoints(), polygon.get('points'));
deepEqual([ { x: 5, y: 7 }, { x: 15, y: 17 } ], polygon.get('points'));
});
test('complexity', function() {
@ -58,7 +59,11 @@
var polygon = new fabric.Polygon(getPoints());
ok(typeof polygon.toObject == 'function');
deepEqual(REFERENCE_OBJECT, polygon.toObject());
var objectWithOriginalPoints = fabric.util.object.extend(polygon.toObject(), {
points: getPoints()
});
deepEqual(objectWithOriginalPoints, REFERENCE_OBJECT);
});
test('fromObject', function() {
@ -89,7 +94,12 @@
elPolygonWithAttrs.setAttribute('transform', 'translate(-10,-20) scale(2)');
var polygonWithAttrs = fabric.Polygon.fromElement(elPolygonWithAttrs);
var expectedPoints = [{x: 10, y: 10}, {x: 20, y: 20}, {x: 30, y: 30}, {x: 10, y: 10}];
var expectedPoints = [
{ x: 0, y: 0 },
{ x: 10, y: 10 },
{ x: 20, y: 20 },
{ x: 0, y: 0 }
];
deepEqual(fabric.util.object.extend(REFERENCE_OBJECT, {
'width': 20,

View file

@ -32,7 +32,8 @@
'hasBorders': true,
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false
'perPixelTargetFind': false,
'shadow': null
};
QUnit.module('fabric.Polyline');
@ -46,7 +47,7 @@
ok(polyline instanceof fabric.Object);
equal(polyline.type, 'polyline');
deepEqual(polyline.get('points'), getPoints());
deepEqual(polyline.get('points'), [ { x: 5, y: 7 }, { x: 15, y: 17 } ]);
});
test('complexity', function() {
@ -57,7 +58,11 @@
test('toObject', function() {
var polyline = new fabric.Polyline(getPoints());
ok(typeof polyline.toObject == 'function');
deepEqual(polyline.toObject(), REFERENCE_OBJECT);
var objectWithOriginalPoints = fabric.util.object.extend(polyline.toObject(), {
points: getPoints()
});
deepEqual(objectWithOriginalPoints, REFERENCE_OBJECT);
});
test('fromObject', function() {

View file

@ -25,6 +25,7 @@
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false,
'shadow': null,
'rx': 0,
'ry': 0
};

34
test/unit/shadow.js Normal file
View file

@ -0,0 +1,34 @@
(function() {
QUnit.module('fabric.Shadow');
test('constructor', function() {
ok(fabric.Shadow);
var shadow = new fabric.Shadow();
ok(shadow instanceof fabric.Shadow, 'should inherit from fabric.Shadow');
});
test('properties', function() {
var shadow = new fabric.Shadow();
equal(shadow.blur, 0);
equal(shadow.color, 'rgb(0,0,0)');
equal(shadow.offsetX, 0);
equal(shadow.offsetY, 0);
});
test('toObject', function() {
var shadow = new fabric.Shadow();
ok(typeof shadow.toObject == 'function');
var object = shadow.toObject();
equal(JSON.stringify(object), '{"color":"rgb(0,0,0)","blur":0,"offsetX":0,"offsetY":0}');
});
// TODO: implement and test this
// test('toSVG', function() {
//
// });
})();

View file

@ -31,6 +31,7 @@
'hasRotatingPoint': true,
'transparentCorners': true,
'perPixelTargetFind': false,
'shadow': null,
'text': 'x',
'fontSize': 40,
'fontWeight': 400,