I have been trying to keep an object (constructed in fabric js over a canvas) inside the boundaries at all the times. It has been achieved at moving and rotating it. I took help from Move object within canvas boundary limit for achieving this. But when I start to scale the object, it simply keeps on going out of boundary. I do not understand what has to be done to keep it inside the boundary only, even while scaling. Please help me with a code to prevent this behavior. It would be great if you can attach a demo too.
<html>
<head>
<title>Basic usage</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.3/fabric.min.js"></script>
</head>
<body>
<canvas id="canvas" style= "border: 1px solid black" height= 480 width = 360></canvas>
<script>
var canvas = new fabric.Canvas('canvas');
canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 }));
canvas.item(0).set({
borderColor: 'gray',
cornerColor: 'black',
cornerSize: 12,
transparentCorners: true
});
canvas.setActiveObject(canvas.item(0));
canvas.renderAll();
canvas.on('object:moving', function (e) {
var obj = e.target;
// if object is too big ignore
if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){
return;
}
obj.setCoords();
// top-left corner
if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){
obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left);
}
// bot-right corner
if(obj.getBoundingRect().top+obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width > obj.canvas.width){
obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top);
obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left);
}
});
</script>
</body>
</html>
My demo is attached here. :
https://jsfiddle.net/3v0cLaLk/
I was able to solve the problem as follows:
var canvas = new fabric.Canvas('canvas');
canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 }));
canvas.item(0).set({
borderColor: 'gray',
cornerColor: 'black',
cornerSize: 12,
transparentCorners: true
});
canvas.setActiveObject(canvas.item(0));
canvas.renderAll();
canvas.on('object:moving', function (e) {
var obj = e.target;
// if object is too big ignore
if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){
return;
}
obj.setCoords();
// top-left corner
if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){
obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left);
}
// bot-right corner
if(obj.getBoundingRect().top+obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width > obj.canvas.width){
obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top);
obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left);
}
});
var left1 = 0;
var top1 = 0 ;
var scale1x = 0 ;
var scale1y = 0 ;
var width1 = 0 ;
var height1 = 0 ;
canvas.on('object:scaling', function (e){
var obj = e.target;
obj.setCoords();
var brNew = obj.getBoundingRect();
if (((brNew.width+brNew.left)>=obj.canvas.width) || ((brNew.height+brNew.top)>=obj.canvas.height) || ((brNew.left<0) || (brNew.top<0))) {
obj.left = left1;
obj.top=top1;
obj.scaleX=scale1x;
obj.scaleY=scale1y;
obj.width=width1;
obj.height=height1;
}
else{
left1 =obj.left;
top1 =obj.top;
scale1x = obj.scaleX;
scale1y=obj.scaleY;
width1=obj.width;
height1=obj.height;
}
});
<html>
<head>
<title>Basic usage</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.3/fabric.min.js"></script>
</head>
<body>
<canvas id="canvas" style= "border: 1px solid black" height= 480 width = 360></canvas>
</body>
</html>
You can set on object modified listener and check if object is out of bounds. If so, then restore it to its original state.
this.canvas.on('object:modified', function (options: any) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > scope.canvas.getWidth()
|| boundingRect.top + boundingRect.height > scope.canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
If you want to perform a real time prevention, you should use object:scaling event, as object:modified is only triggered at the end of the transformation.
1) Add event handler to canvas:
this.canvas.on('object:scaling', (e) => this._handleScaling(e));
2) In the handler function, get the old and the new object's bounding rect:
_handleScaling(e) {
var obj = e.target;
var brOld = obj.getBoundingRect();
obj.setCoords();
var brNew = obj.getBoundingRect();
3) For each border, check if object has scaled beyond the canvas boundaries and compute its left, top and scale properties:
// left border
// 1. compute the scale that sets obj.left equal 0
// 2. compute height if the same scale is applied to Y (we do not allow non-uniform scaling)
// 3. compute obj.top based on new height
if(brOld.left >= 0 && brNew.left < 0) {
let scale = (brOld.width + brOld.left) / obj.width;
let height = obj.height * scale;
let top = ((brNew.top - brOld.top) / (brNew.height - brOld.height) *
(height - brOld.height)) + brOld.top;
this._setScalingProperties(0, top, scale);
}
4) Similar code for the other borders:
// top border
if(brOld.top >= 0 && brNew.top < 0) {
let scale = (brOld.height + brOld.top) / obj.height;
let width = obj.width * scale;
let left = ((brNew.left - brOld.left) / (brNew.width - brOld.width) *
(width - brOld.width)) + brOld.left;
this._setScalingProperties(left, 0, scale);
}
// right border
if(brOld.left + brOld.width <= obj.canvas.width
&& brNew.left + brNew.width > obj.canvas.width) {
let scale = (obj.canvas.width - brOld.left) / obj.width;
let height = obj.height * scale;
let top = ((brNew.top - brOld.top) / (brNew.height - brOld.height) *
(height - brOld.height)) + brOld.top;
this._setScalingProperties(brNew.left, top, scale);
}
// bottom border
if(brOld.top + brOld.height <= obj.canvas.height
&& brNew.top + brNew.height > obj.canvas.height) {
let scale = (obj.canvas.height - brOld.top) / obj.height;
let width = obj.width * scale;
let left = ((brNew.left - brOld.left) / (brNew.width - brOld.width) *
(width - brOld.width)) + brOld.left;
this._setScalingProperties(left, brNew.top, scale);
}
5) If object's BoundingRect has crossed canvas boundaries, fix its position and scale:
if(brNew.left < 0
|| brNew.top < 0
|| brNew.left + brNew.width > obj.canvas.width
|| brNew.top + brNew.height > obj.canvas.height) {
obj.left = this.scalingProperties['left'];
obj.top = this.scalingProperties['top'];
obj.scaleX = this.scalingProperties['scale'];
obj.scaleY = this.scalingProperties['scale'];
obj.setCoords();
} else {
this.scalingProperties = null;
}
}
6) Finally, when setting the scaling properties, we have to stick with the smallest scale in case the object has crossed more than one border:
_setScalingProperties(left, top, scale) {
if(this.scalingProperties == null
|| this.scalingProperties['scale'] > scale) {
this.scalingProperties = {
'left': left,
'top': top,
'scale': scale
};
}
}
Below is the code for blocking the coordinates of any object outside the canvas area from all directions
canvas.on('object:modified', function (data) {
var currentObject = data.target;
var tempObject = angular.copy(data.target);
var canvasMaxWidth = canvas.width - 20,
canvasMaxHeight = canvas.height - 20;
var actualWidth = currentObject.getBoundingRect().width,
actualHeight = currentObject.getBoundingRect().height;
if (actualHeight > canvasMaxHeight) {
currentObject.scaleToHeight(canvasMaxHeight);
currentObject.setCoords();
canvas.renderAll();
if (tempObject.scaleX < currentObject.scaleX) {
currentObject.scaleX = tempObject.scaleX;
currentObject.setCoords();
canvas.renderAll();
}
if (tempObject.scaleY < currentObject.scaleY) {
currentObject.scaleY = tempObject.scaleY;
currentObject.setCoords();
canvas.renderAll();
}
if (currentObject.getBoundingRectHeight() < canvasMaxHeight - 50) {
currentObject.scaleX = (currentObject.scaleX * canvasMaxHeight) / (currentObject.scaleX * currentObject.width);
currentObject.setCoords();
canvas.renderAll();
}
}
if (actualWidth > canvasMaxWidth) {
currentObject.scaleToWidth(canvasMaxWidth);
obj.setCoords();
canvas.renderAll();
if (tempObject.scaleX < currentObject.scaleX) {
currentObject.scaleX = tempObject.scaleX;
currentObject.setCoords();
canvas.renderAll();
}
if (tempObject.scaleY < currentObject.scaleY) {
currentObject.scaleY = tempObject.scaleY;
currentObject.setCoords();
canvas.renderAll();
}
}
obj.setCoords();
canvas.renderAll();
});
I was able to block movement outside of boundaries using the Bounding box in the following way using the last version of Fabric ("fabric": "^4.6.0") & Typescript:
private boundingBox: fabric.Rect = null;
this.setBoundingBox(width, height);
private setBoundingBox(width: number, height: number) {
this.boundingBox = new fabric.Rect({
name: OBJECT_TYPE.BOUNDING_BOX,
fill: DEFINITIONS.BG_COLOR,
width: width,
height: height,
hasBorders: false,
hasControls: false,
lockMovementX: true,
lockMovementY: true,
selectable: false,
evented: false,
stroke: 'red',
});
this._canvas.add(this.boundingBox);
}
this._canvas.on('object:moving', (e) => {
console.log('object:moving');
this._avoidObjectMovingOutsideOfBoundaries(e);
});
private _avoidObjectMovingOutsideOfBoundaries(e: IEvent) {
let obj = e.target;
const top = obj.top;
const bottom = top + obj.height;
const left = obj.left;
const right = left + obj.width;
const topBound = this.boundingBox.top;
const bottomBound = topBound + this.boundingBox.height;
const leftBound = this.boundingBox.left;
const rightBound = leftBound + this.boundingBox.width;
obj.left = Math.min(Math.max(left, leftBound), rightBound - obj.width);
obj.top = Math.min(Math.max(top, topBound), bottomBound - obj.height);
return obj;
}
Any additional extensions for Scaling objects are welcome.
canvas.on('object:scaling', function (e) {
var obj = e.target;
obj.setCoords();
let top = obj.getBoundingRect().top;
let left = obj.getBoundingRect().left;
let height = obj.getBoundingRect().height;
let width = obj.getBoundingRect().width;
// restrict scaling below bottom of canvas
if (top + height > CANVAS_HEIGHT) {
obj.scaleY = 1;
obj.setCoords();
let h = obj.getScaledHeight();
obj.scaleY = (CANVAS_HEIGHT - top) / h;
obj.setCoords();
canvas.renderAll();
obj.lockScalingX = true;
obj.lockScalingY = true;
obj.lockMovementX = true;
obj.lockMovementY = true;
}
// restrict scaling above top of canvas
if (top < 0) {
obj.scaleY = 1;
obj.setCoords();
let h = obj.getScaledHeight();
obj.scaleY = (height + top) / h;
obj.top = 0;
obj.setCoords();
canvas.renderAll();
obj.lockScalingX = true;
obj.lockScalingY = true;
obj.lockMovementX = true;
obj.lockMovementY = true;
}
// restrict scaling over right of canvas
if (left + width > CANVAS_WIDTH) {
obj.scaleX = 1;
obj.setCoords();
let w = obj.getScaledWidth();
obj.scaleX = (CANVAS_WIDTH - left) / w;
obj.setCoords();
canvas.renderAll();
obj.lockScalingX = true;
obj.lockScalingY = true;
obj.lockMovementX = true;
obj.lockMovementY = true;
}
// restrict scaling over left of canvas
if (left < 0) {
obj.scaleX = 1;
obj.setCoords();
let w = obj.getScaledWidth();
obj.scaleX = (width + left) / w;
obj.left = 0;
obj.setCoords();
canvas.renderAll();
obj.lockScalingX = true;
obj.lockScalingY = true;
obj.lockMovementX = true;
obj.lockMovementY = true;
}
});
canvas.on('object:modified', function (event) {
// after text object is done with modifing e.g. resizing or moving
if (!!event.target) {
event.target.lockScalingX = false;
event.target.lockScalingY = false;
event.target.lockMovementX = false;
event.target.lockMovementY = false;
}
})
Related
I am using firefox to open my page but when I open multiple tabs, the browser console error "Uncaught TypeError: this.containerDim is undefined". If I only open 1 tab then it works fine. This doesn't happen in chrome. Can someone answer for me?
Here is full code error
/*** picture view plugin ****/
(function ($, window, document, undefined) {
"use strict";
//an empty function
var noop = function () {};
var $body = $('body'),
$window = $(window),
$document = $(document);
//constants
var ZOOM_CONSTANT = 15; //increase or decrease value for zoom on mouse wheel
var MOUSE_WHEEL_COUNT = 5; //A mouse delta after which it should stop preventing default behaviour of mouse wheel
//ease out method
/*
t : current time,
b : intial value,
c : changed value,
d : duration
*/
function easeOutQuart(t, b, c, d) {
t /= d;
t--;
return -c * (t * t * t * t - 1) + b;
};
//function to check if image is loaded
function imageLoaded(img) {
return img.complete && (typeof img.naturalWidth === 'undefined' | img.naturalWidth !== 0);
}
var imageViewHtml = '<div class="iv-loader-ss"></div> <div class="iv-snap-view-ss">' + '<div class="iv-snap-image-wrap-ss">' + '<div class="iv-snap-handle-ss"></div>' + '</div>' + '<div class="iv-zoom-slider-ss" id="slider_zoom"><div class="iv-zoom-handle-ss"></div></div></div>' + '<div class="iv-image-view-ss" ><div class="iv-image-wrap-ss" ></div></div>';
//var imageViewHtml = '<div class="iv-zoom-slider id="slider_zoom"><div class="iv-zoom-handle"></div></div>';
var slider_zoom = '<div class="iv-zoom-slider">' + '<div class="iv-zoom-handle"></div>' + '</div>';
function Slider(container, options) {
this.container = container;
this.onStart = options.onStart || noop;
this.onMove = options.onMove || noop;
this.onEnd = options.onEnd || noop;
this.sliderId = options.sliderId || 'slider' + Math.ceil(Math.random() * 1000000);
}
Slider.prototype.init = function () {
var self = this,
container = this.container,
eventSuffix = '.' + this.sliderId;
//assign event on snap image wrap
this.container.on('touchstart' + eventSuffix + ' mousedown' + eventSuffix, function (estart) {
estart.preventDefault();
var touchMove = (estart.type == "touchstart" ? "touchmove" : "mousemove") + eventSuffix,
touchEnd = (estart.type == "touchstart" ? "touchend" : "mouseup") + eventSuffix,
eOrginal = estart.originalEvent,
sx = eOrginal.clientX || eOrginal.touches[0].clientX,
sy = eOrginal.clientY || eOrginal.touches[0].clientY;
var start = self.onStart(estart, {
x: sx,
y: sy
});
if (start === false) return;
var moveListener = function (emove) {
emove.preventDefault();
eOrginal = emove.originalEvent;
//get the cordinates
var mx = eOrginal.clientX,
my = eOrginal.clientY;
self.onMove(emove, {
dx: mx - sx,
dy: my - sy,
mx: mx,
my: my
});
};
var endListener = function () {
$document.off(touchMove, moveListener);
$document.off(touchEnd, endListener);
self.onEnd();
};
$document.on(touchMove, moveListener);
$document.on(touchEnd, endListener);
});
return this;
}
function ImageViewerss(container, options) {
var self = this;
if (container.is('#iv-container-ss')) {
self._fullPage = true;
}
self.container = container;
options = self.options = $.extend({}, ImageViewerss.defaults, options);
self.zoomValue = 100;
if (!container.find('.snap-view').length) {
//container.append(slider_zoom)
container.prepend(imageViewHtml);
}
container.addClass('iv-container-ss');
if (container.css('position') == 'static') container.css('position', 'relative');
self.snapView = container.find('.iv-snap-view-ss');
self.snapImageWrap = container.find('.iv-snap-image-wrap-ss');
self.imageWrap = container.find('.iv-image-wrap-ss');
self.snapHandle = container.find('.iv-snap-handle-ss');
self.zoomHandle = container.find('.iv-zoom-handle-ss');
self._viewerId = 'iv' + Math.floor(Math.random() * 1000000);
}
ImageViewerss.prototype = {
constructor: ImageViewerss,
_init: function () {
var viewerss = this,
options = viewerss.options,
zooming = false, // tell weather we are zooming trough touch
container = this.container;
var eventSuffix = '.' + viewerss._viewerId;
//cache dom refrence
var snapHandle = this.snapHandle;
var snapImgWrap = this.snapImageWrap;
var imageWrap = this.imageWrap;
var snapSlider = new Slider(snapImgWrap, {
sliderId: viewerss._viewerId,
onStart: function () {
if (!viewerss.loaded) return false;
var handleStyle = snapHandle[0].style;
this.curHandleTop = parseFloat(handleStyle.top);
this.curHandleLeft = parseFloat(handleStyle.left);
this.handleWidth = parseFloat(handleStyle.width);
this.handleHeight = parseFloat(handleStyle.height);
this.width = snapImgWrap.width();
this.height = snapImgWrap.height();
//stop momentum on image
clearInterval(imageSlider.slideMomentumCheck);
cancelAnimationFrame(imageSlider.sliderMomentumFrame);
},
onMove: function (e, position) {
var xPerc = this.curHandleLeft + position.dx * 100 / this.width,
yPerc = this.curHandleTop + position.dy * 100 / this.height;
xPerc = Math.max(0, xPerc);
xPerc = Math.min(100 - this.handleWidth, xPerc);
yPerc = Math.max(0, yPerc);
yPerc = Math.min(100 - this.handleHeight, yPerc);
var containerDim = viewerss.containerDim,
imgWidth = viewerss.imageDim.w * (viewerss.zoomValue / 100),
imgHeight = viewerss.imageDim.h * (viewerss.zoomValue / 100),
imgLeft = imgWidth < containerDim.w ? (containerDim.w - imgWidth) / 2 : -imgWidth * xPerc / 100,
imgTop = imgHeight < containerDim.h ? (containerDim.h - imgHeight) / 2 : -imgHeight * yPerc / 100;
snapHandle.css({
top: yPerc + '%',
left: xPerc + '%'
})
viewerss.currentImg.css({
left: imgLeft,
top: imgTop
})
viewerss.compareImg.css({
left: imgLeft,
top: imgTop
})
}
}).init();
/*Add slide interaction to image*/
var imageSlider = viewerss._imageSlider = new Slider(imageWrap, {
sliderId: viewerss._viewerId,
onStart: function (e, position) {
if (!viewerss.loaded) return false;
if (zooming) return;
var self = this;
snapSlider.onStart();
self.imgWidth = viewerss.imageDim.w * viewerss.zoomValue / 100;
self.imgHeight = viewerss.imageDim.h * viewerss.zoomValue / 100;
self.positions = [position, position];
self.startPosition = position;
},
onMove: function (e, position) {
if (zooming) return;
this.currentPos = position;
snapSlider.onMove(e, {
dx: -position.dx * snapSlider.width / this.imgWidth,
dy: -position.dy * snapSlider.height / this.imgHeight
});
},
}).init();
/*Add zoom interation in mouse wheel*/
var changedDelta = 0;
imageWrap.on("mousewheel" + eventSuffix + " DOMMouseScroll" + eventSuffix, function (e) {
if(!options.zoomOnMouseWheel) return;
if (!viewerss.loaded) return;
//clear all animation frame and interval
viewerss._clearFrames();
// cross-browser wheel delta
var delta = Math.max(-1, Math.min(1, (e.originalEvent.wheelDelta || -e.originalEvent.detail))),
zoomValue = viewerss.zoomValue * (100 + delta * ZOOM_CONSTANT) / 100;
if(!(zoomValue >= 100 && zoomValue <= options.maxZoom)){
changedDelta += Math.abs(delta);
}
else{
changedDelta = 0;
}
if(changedDelta > MOUSE_WHEEL_COUNT) return;
e.preventDefault();
var contOffset = container.offset(),
x = (e.pageX || e.originalEvent.pageX) - contOffset.left,
y = (e.pageY || e.originalEvent.pageY) - contOffset.top;
viewerss.zoom(zoomValue, {
x: x,
y: y
});
});
//zoom in zoom out using zoom handle
var slider = viewerss.snapView.find('.iv-zoom-slider-ss');
var zoomSlider = new Slider(slider, {
sliderId: viewerss._viewerId,
onStart: function (eStart) {
if (!viewerss.loaded) return false;
this.leftOffset = slider.offset().left;
this.handleWidth = viewerss.zoomHandle.width();
this.onMove(eStart);
},
onMove: function (e, position) {
var newLeft = (e.pageX || e.originalEvent.touches[0].pageX) - this.leftOffset - this.handleWidth / 2;
newLeft = Math.max(0, newLeft);
newLeft = Math.min(viewerss._zoomSliderLength, newLeft);
var zoomValue = 100 + (options.maxZoom - 100) * newLeft / viewerss._zoomSliderLength;
viewerss.zoom(zoomValue);
}
}).init();
//calculate elments size on window resize
if (options.refreshOnResize) $window.on('resize' + eventSuffix, function () {
//viewerss.refresh()
});
},
//method to zoom images
zoom: function (perc, point) {
perc = Math.max(100, perc);
if ((mouse_hover_depth & show_snap_view) | (!show_snap_view & hover_my & !hover_hang2)){
point = {
x: point_x_depth,
y: point_y_depth
};
console.log("check run if");
console.log("containerDim 00: "+this.containerDim.w);
}else{
console.log("check run else");
point = point || {
x: this.containerDim.w / 2,
y: this.containerDim.h / 2
};
console.log("containerDim 01: "+this.containerDim.w);
console.log("containerDim 02: "+this.containerDim.h);
}
point_x_ir = point.x;
point_y_ir = point.y;
var self = this,
maxZoom = this.options.maxZoom,
curPerc = this.zoomValue,
curImg = this.currentImg,
compareImg = this.compareImg,
containerDim = this.containerDim,
curLeft = parseFloat(curImg.css('left')),
curTop = parseFloat(curImg.css('top'));
console.log("containerDim 1: "+containerDim);
self._clearFrames();
var step = 0;
//calculate base top,left,bottom,right
var containerDim = self.containerDim,
imageDim = self.imageDim;
console.log("self.imageDim: "+self.imageDim);
console.log("self.containerDim 2: "+self.containerDim);
console.log("containerDim 2: "+containerDim);
var baseLeft = (containerDim.w - imageDim.w) / 2,
baseTop = (containerDim.h - imageDim.h) / 2,
baseRight = containerDim.w - baseLeft,
baseBottom = containerDim.h - baseTop;
function zoom() {
step++;
if (step < 20) {
self._zoomFrame = requestAnimationFrame(zoom);
}
var tickZoom = Math.min(maxZoom, easeOutQuart(step, curPerc, perc - curPerc, 20));
var ratio = tickZoom / curPerc,
imgWidth = self.imageDim.w * tickZoom / 100,
imgHeight = self.imageDim.h * tickZoom / 100,
newLeft = -((point.x - curLeft) * ratio - point.x),
newTop = -((point.y - curTop) * ratio - point.y);
/
//fix for left and top
newLeft = Math.min(newLeft, baseLeft);
newTop = Math.min(newTop, baseTop);
//fix for right and bottom
if((newLeft + imgWidth) < baseRight){
newLeft = (self.containerDim.w - imgWidth) ; //newLeft - (newLeft + imgWidth - baseRig
}
if((newTop + imgHeight) < baseBottom){
newTop = (self.containerDim.h - imgHeight) ; //newLeft - (newLeft + imgWidth - baseRig
}
curImg.css({
height: imgHeight + 'px',
width: imgWidth + 'px',
left: newLeft + 'px',
top: newTop + 'px'
});
compareImg.css({
height: 'auto',
width: imgWidth + 'px',
left: newLeft + 'px',
top: newTop + 'px'
});
self.zoomValue = tickZoom;
self._resizeHandle(imgWidth, imgHeight, newLeft, newTop);
//update zoom handle position
self.zoomHandle.css('left', ((tickZoom - 100) * self._zoomSliderLength) / (maxZoom - 100) + 'px');
}
zoom();
},
_clearFrames: function () {
//clearInterval(this._imageSlider.slideMomentumCheck);
cancelAnimationFrame(this._imageSlider.sliderMomentumFrame);
cancelAnimationFrame(this._zoomFrame)
},
//ko có vẫn chạy đc
resetZoom: function () {
this.zoom(this.options.zoomValue);
},
//calculate dimensions of image, container and reset the image
_calculateDimensions: function () {
//calculate content width of image and snap image
var self = this,
curImg = self.currentImg,
compareImg = self.compareImg,
container = self.container,
snapView = self.snapView,
imageWidth = curImg.width(),
imageHeight = curImg.height(),
contWidth = container.width(),
contHeight = container.height(),
snapViewWidth = snapView.innerWidth(),
snapViewHeight = snapView.innerHeight();
//set the container dimension
self.containerDim = {
w: contWidth,
h: contHeight
}
//set the image dimension
var imgWidth, imgHeight, ratio = imageWidth / imageHeight;
imgWidth = (imageWidth > imageHeight && contHeight >= contWidth) || ratio * contHeight > contWidth ? contWidth : ratio * contHeight;
imgHeight = imgWidth / ratio;
self.imageDim = {
w: imgWidth,
h: imgHeight
}
compareImg.css({
width: imgWidth + 'px',
height: 'auto',
left: (contWidth - imgWidth) / 2 + 'px',
top: (contHeight - imgHeight) / 2 + 'px',
'max-width': 'none',
'max-height': 'none'
});
curImg.css({
width: imgWidth + 'px',
height: imgHeight + 'px',
left: (contWidth - imgWidth) / 2 + 'px',
top: (contHeight - imgHeight) / 2 + 'px',
'max-width': 'none',
'max-height': 'none'
});
//set the snap Image dimension
var snapWidth = imgWidth > imgHeight ? snapViewWidth : imgWidth * snapViewHeight / imgHeight,
snapHeight = imgHeight > imgWidth ? snapViewHeight : imgHeight * snapViewWidth / imgWidth;
//ko có cũng ko sao
self.snapImageDim = {
w: snapWidth,
h: snapHeight
}
self.snapImg.css({
width: snapWidth,
height: snapHeight
});
//calculate zoom slider area
self._zoomSliderLength = snapViewWidth - self.zoomHandle.outerWidth();
},
refresh: function () {
if (!this.loaded) return;
this._calculateDimensions();
this.resetZoom();
},
_resizeHandle: function (imgWidth, imgHeight, imgLeft, imgTop) {
var curImg = this.currentImg,
imageWidth = imgWidth || this.imageDim.w * this.zoomValue / 100,
imageHeight = imgHeight || this.imageDim.h * this.zoomValue / 100,
left = Math.max(-(imgLeft || parseFloat(curImg.css('left'))) * 100 / imageWidth, 0),
top = Math.max(-(imgTop || parseFloat(curImg.css('top'))) * 100 / imageHeight, 0),
handleWidth = Math.min(this.containerDim.w * 100 / imageWidth, 100),
handleHeight = Math.min(this.containerDim.h * 100 / imageHeight, 100);
this.snapHandle.css({
top: top + '%',
left: left + '%',
width: handleWidth + '%',
height: handleHeight + '%'
});
},
load: function (image, hiResImg, compare, hiResCompare) {
var self = this,
container = this.container;
container.find('.iv-snap-image-ss,.iv-large-image-ss').remove();
var snapImageWrap = this.container.find('.iv-snap-image-wrap-ss');
snapImageWrap.prepend('<img class="iv-snap-image-ss" style="width:120px; height: 90px;" src="' + image + '" />');
this.imageWrap.prepend('<img class="iv-large-image-ss" src="' + image + '" />');
if (hiResImg) {
this.imageWrap.append('<img class="iv-large-image-ss" src="' + hiResImg + '" />')
}
if(compare) {
this.imageWrap.append('<img class="iv-large-compare-ss" src="' + compare + '" />');
}
var currentImg = this.currentImg = this.container.find('.iv-large-image-ss');
var compareImg = this.compareImg = this.container.find('.iv-large-compare');
this.snapImg = this.container.find('.iv-snap-image-ss');
self.loaded = false;
//show loader
container.find('.iv-loader-ss').show();
currentImg.hide();
compareImg.css('opacity', 0);
self.snapImg.hide();
//refresh the view
function refreshView() {
self.loaded = true;
self.zoomValue = 100;
//reset zoom of images
currentImg.show();
self.snapImg.show();
self.refresh();
self.resetZoom();
//hide loader
container.find('.iv-loader-ss').hide();
}
if (imageLoaded(currentImg[0])) {
refreshView();
} else {
$(currentImg[0]).on('load', refreshView);
}
},
setCompareAlpha: function(alpha) {
/*Add shift binding for image compare toggle*/
this.compareImg.css('opacity', alpha)
}
}
ImageViewerss.defaults = {
zoomValue: 100,
snapView: true,
maxZoom: 300,
refreshOnResize: true,
zoomOnMouseWheel : true
}
window.ImageViewerss = function (container, options) {
var imgElm, imgSrc, compareElm, compareSrc, hiResImg, hiResCompare;
if (!(container && (typeof container == "string" || container instanceof Element || container[0] instanceof Element))) {
options = container;
container = $('#iv-container-ss');
}
container = $(container);
if (container.is('img')) {
imgElm = container;
imgSrc = imgElm[0].src;
hiResImg = imgElm.attr('high-res-src') || imgElm.attr('data-high-res-src');
container = imgElm.wrap('<div class="iv-container-ss" style="display:inline-block; overflow:hidden"></div>').parent();
imgElm.css({
opacity: 0,
position: 'relative',
zIndex: -1
});
} else {
imgSrc = container.attr('src') || container.attr('data-src');
compareSrc = container.attr('compare-src') || container.attr('data-compare-src');
hiResImg = container.attr('high-res-src') || container.attr('data-high-res-src');
}
var viewerss = new ImageViewerss(container, options);
viewerss._init();
if (imgSrc) viewerss.load(imgSrc, hiResImg, compareSrc, hiResCompare);
return viewerss;
};
}((window.jQuery), window, document));
Here is the error code(The error line is the first line of the code below) :
//calculate base top,left,bottom,right
var containerDim = self.containerDim,
imageDim = self.imageDim;
console.log("self.imageDim: "+self.imageDim);
console.log("self.containerDim 2: "+self.containerDim);
console.log("containerDim 2: "+containerDim);
var baseLeft = (containerDim.w - imageDim.w) / 2,
baseTop = (containerDim.h - imageDim.h) / 2,
baseRight = containerDim.w - baseLeft,
baseBottom = containerDim.h - baseTop;
I'm call it like this:
if (mouseDown) {
ismouse = true
} else {
if (ismouse) {
$('.iv-large-image-ss').css('top', $('.iv-large-image').css('top'));
$('.iv-large-image-ss').css('left', $('.iv-large-image').css('left'));
ismouse = false
}
viewerss.zoom(rate);
}
"rate" is a number that I want to assign to represent the zoom level.
I tried to clear the firefox cache and ran the code above, but every time I open a new tab I have to clear the cache to run. I don't know why! :((
I am trying to get 3d transform effect of the bacground image on mouse move.
I have checked f.e. jquery.plate tilt.js and many others plugins, BUT, each of them has problem on chronium browsers like Chrome or Opera (it works fine even on IE11 -.-)
See the attachment and please adwise what is that and if it is fixable? The "dots" appear on mouse move (when background moves) randomly but in line within image.
(function($) {
'use strict';
var namespace = 'jquery-plate';
function Plate($element, options) {
this.config(options);
this.$container = $element;
if (this.options.element) {
if (typeof this.options.element === 'string') {
this.$element = this.$container.find(this.options.element);
} else {
this.$element = $(this.options.element);
}
} else {
this.$element = $element;
}
this.originalTransform = this.$element.css('transform');
this.$container
.on('mouseenter.' + namespace, this.onMouseEnter.bind(this))
.on('mouseleave.' + namespace, this.onMouseLeave.bind(this))
.on('mousemove.' + namespace, this.onMouseMove.bind(this));
}
Plate.prototype.config = function(options) {
this.options = $.extend({
inverse: false,
perspective: 500,
maxRotation: 10,
animationDuration: 200
}, this.options, options);
};
Plate.prototype.destroy = function() {
this.$element.css('transform', this.originalTransform);
this.$container.off('.' + namespace);
};
Plate.prototype.update = function(offsetX, offsetY, duration) {
var rotateX;
var rotateY;
if (offsetX || offsetX === 0) {
var height = this.$container.outerHeight();
var py = (offsetY - height / 2) / (height / 2);
rotateX = this.round(this.options.maxRotation * -py);
} else {
rotateY = 0;
}
if (offsetY || offsetY === 0) {
var width = this.$container.outerWidth();
var px = (offsetX - width / 2) / (width / 2);
rotateY = this.round(this.options.maxRotation * px);
} else {
rotateX = 0;
}
if (this.options.inverse) {
rotateX *= -1;
rotateY *= -1;
}
if (duration) {
this.animate(rotateX, rotateY, duration);
} else if (this.animation && this.animation.remaining) {
this.animation.targetX = rotateX;
this.animation.targetY = rotateY;
} else {
this.transform(rotateX, rotateY);
}
};
Plate.prototype.reset = function(duration) {
this.update(null, null, duration);
};
Plate.prototype.transform = function(rotateX, rotateY) {
this.currentX = rotateX;
this.currentY = rotateY;
this.$element.css('transform',
(this.originalTransform && this.originalTransform !== 'none' ? this.originalTransform + ' ' : '') +
'perspective(' + this.options.perspective + 'px) ' +
'rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg)'
);
};
Plate.prototype.animate = function(rotateX, rotateY, duration) {
if (duration) {
this.animation = this.animation || {};
var remaining = this.animation.remaining;
this.animation.time = performance.now();
this.animation.remaining = duration || null;
this.animation.targetX = rotateX;
this.animation.targetY = rotateY;
if (!remaining) {
requestAnimationFrame(this.onAnimationFrame.bind(this));
}
} else {
this.transform(rotateX, rotateY);
}
};
Plate.prototype.round = function(number) {
return Math.round(number * 1000) / 1000;
};
Plate.prototype.offsetCoords = function(event) {
var offset = this.$container.offset();
return {
x: event.pageX - offset.left,
y: event.pageY - offset.top
};
};
Plate.prototype.onAnimationFrame = function(timestamp) {
this.animation = this.animation || {};
var delta = timestamp - (this.animation.time || 0);
this.animation.time = timestamp;
var duration = this.animation.remaining || 0;
var percent = Math.min(delta / duration, 1);
var currentX = this.currentX || 0;
var currentY = this.currentY || 0;
var targetX = this.animation.targetX || 0;
var targetY = this.animation.targetY || 0;
var rotateX = this.round(currentX + (targetX - currentX) * percent);
var rotateY = this.round(currentY + (targetY - currentY) * percent);
this.transform(rotateX, rotateY);
var remaining = duration - delta;
this.animation.remaining = remaining > 0 ? remaining : null;
if (remaining > 0) {
requestAnimationFrame(this.onAnimationFrame.bind(this));
}
};
Plate.prototype.onMouseEnter = function(event) {
var offset = this.offsetCoords(event);
this.update(offset.x, offset.y, this.options.animationDuration);
};
Plate.prototype.onMouseLeave = function(event) {
this.reset(this.options.animationDuration);
};
Plate.prototype.onMouseMove = function(event) {
var offset = this.offsetCoords(event);
this.update(offset.x, offset.y);
};
$.fn.plate = function(options) {
return this.each(function() {
var $element = $(this);
var plate = $element.data(namespace);
if (options === 'remove') {
plate.destroy();
$element.data(namespace, null);
} else {
if (!plate) {
plate = new Plate($element, options);
$element.data(namespace, plate);
plate.reset();
} else {
plate.config(options);
}
}
});
};
})(jQuery);
$('#ab12cd').plate()
<div id="ab12cd" styles="width:100%;height:100%">
<img src="http://eskipaper.com/images/dark-background-8.jpg" />
</div>
<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
// please open in new window, most visible effect you cen see if you move mouse bottom left/right
I am trying to make a quadratic curved arrow tool.
I was used demo in the following link for creating the tool.
http://kpomservices.com/oldweb/HTML5_Canvas_Curved_Lines.php
i was success to create an tool as I needs.
but I am getting some issues with that.
Hope someone here can help me on that issues...
here is the code I am using for creating the tool ..
line_number++;
var line;
var reinit_stroke = "";
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
function _getQBezierValue(t, p1, p2, p3) {
var iT = 1 - t;
return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3;
}
function getQuadraticCurvePoint(startX, startY, cpX, cpY, endX, endY, position) {
return {
x: _getQBezierValue(position, startX, cpX, endX),
y: _getQBezierValue(position, startY, cpY, endY)
};
}
canvas.on({
'object:selected': onObjectSelected,
'object:moving': onObjectMoving,
'before:selection:cleared': onBeforeSelectionCleared
});
(function drawQuadratic() {
line = new fabric.Path('M 65 0 Q 100, 100, 200, 0', { fill: '', selectable: false,hasBorders: false,hasControls: false,stroke: #000});
line.path[0][1] = posx;
line.path[0][2] = posy;
line.path[1][1] = posx+50;
line.path[1][2] = posy+50;
line.path[1][3] = posx+100;
line.path[1][4] = posy+100;
line.id = line_number;
//line.selectable = false;
canvas.add(line);
canvas.sendBackwards(line);
var pt = getQuadraticCurvePoint(line.path[0][1], line.path[0][2], line.path[1][1], line.path[1][2], line.path[1][3], line.path[1][4], 0.5);
var p1 = makeCurvePoint(pt.x, pt.y, null, line, null)
p1.name = "p1";
p1.id = line_number;
canvas.add(p1);
var p0 = makeCurveCircle(posx, posy, line, p1, null);
p0.name = "p0";
p0.id = line_number;
canvas.add(p0);
var p2 = makeArrow(posx+100, posy+100, null, p1, line);
p2.name = "p2";
p2.id = line_number;
canvas.add(p2);
var dx = line.path[1][3] - posx;
var dy = line.path[1][4] - posy;
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
p2.setAngle(angle + 90);
p2.setCoords();
canvas.bringToFront(p2);
canvas.bringToFront(p0);
canvas.bringToFront(p1);
line.p2 = p2;
})();
function makeArrow(left, top, line1, line2, line3) {
var c = new fabric.Triangle({
width: 5,
height: 5,
left: left+5,
top: top+5,
strokeWidth: 10,
fill: #000,
opacity: 1,
stroke: #000
});
c.hasBorders = c.hasControls = false;
c.angle = 90;
c.line1 = line1;
c.line2 = line2;
c.line3 = line3;
return c;
}
function makeCurveCircle(left, top, line1, line2, line3) {
var c = new fabric.Circle({
radius: 5,
left: left,
top: top,
strokeWidth: 10,
fill: #000,
stroke: #000
});
c.hasBorders = c.hasControls = false;
c.line1 = line1;
c.line2 = line2;
c.line3 = line3;
return c;
}
function makeCurvePoint(left, top, line1, line2, line3) {
var c = new fabric.Circle({
radius: 5,
left: left,
top: top,
strokeWidth: 10,
fill: #000,
opacity: 0,
stroke: #000
});
c.hasBorders = c.hasControls = false;
c.angle = 90;
c.line1 = line1;
c.line2 = line2;
c.line3 = line3;
return c;
}
var prevselobj;
function onObjectSelected(e) {
var activeObject = e.target;
reinit_stroke = activeObject.stroke;
if (activeObject.name == "p0" || activeObject.name == "p2") {
if (prevselobj) {
prevselobj.line2.animate('opacity', '0', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
prevselobj.line2.selectable = false;
}
activeObject.line2.animate('opacity', '1', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
activeObject.line2.selectable = true;
prevselobj = activeObject;
}
}
function onBeforeSelectionCleared(e) {
var activeObject = e.target;
if (activeObject.name == "p0" || activeObject.name == "p2") {
activeObject.line2.animate('opacity', '0', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
activeObject.line2.selectable = false;
}
else if (activeObject.name == "p1") {
activeObject.animate('opacity', '0', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
activeObject.selectable = true;
}
}
function onObjectMoving(e) {
if (e.target.name == "p0" || e.target.name == "p2") {
var p = e.target;
var curvedline;
if (p.line1) {
p.line1.path[0][1] = p.left;
p.line1.path[0][2] = p.top + p.height/2;
curvedline = p.line1;
} else if (p.line3) {
p.line3.path[1][3] = p.left;
if(p.line3.path[0][2] <= p.line3.path[1][4])
p.line3.path[1][4] = p.top - p.height/2;
if(p.line3.path[0][2] > p.line3.path[1][4])
p.line3.path[1][4] = p.top + p.height/2;
p.line3.setCoords();
curvedline = p.line3;
}
if (curvedline) {
curvedline.setCoords();
var pt = getQuadraticCurvePoint(curvedline.path[0][1], curvedline.path[0][2], curvedline.path[1][1], curvedline.path[1][2], curvedline.path[1][3], curvedline.path[1][4], 0.5);
p.line2.left = pt.x;
p.line2.top = pt.y;
if (curvedline.p2) {
var pt = getQuadraticCurvePoint(curvedline.path[0][1], curvedline.path[0][2], curvedline.path[1][1], curvedline.path[1][2], curvedline.path[1][3], curvedline.path[1][4], 0.99);
curvedline.p2.left = pt.x;
curvedline.p2.top = pt.y;
var dx = curvedline.path[1][3] - pt.x;
var dy = curvedline.path[1][4] - pt.y;
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
curvedline.p2.setAngle(angle + 90);
curvedline.p2.setCoords();
}
p.line2.setCoords();
}
if (e.target.text) {
e.target.text.left = p.left;
e.target.text.top = p.top;
e.target.text.setCoords();
}
} else if (e.target.name == "p1") {
var p = e.target;
if (p.line2) {
p.line2.path[1][1] = p.left;
p.line2.path[1][2] = p.top;
}
curvedline = p.line2;
if (curvedline) {
var pt = getQuadraticCurvePoint(curvedline.path[0][1], curvedline.path[0][2], curvedline.path[1][1], curvedline.path[1][2], curvedline.path[1][3], curvedline.path[1][4], 0.5);
p.left = pt.x;
p.top = pt.y;
p.setCoords();
}
if (curvedline) {
var pt = getQuadraticCurvePoint(curvedline.path[0][1], curvedline.path[0][2], curvedline.path[1][1], curvedline.path[1][2], curvedline.path[1][3], curvedline.path[1][4], 0.99);
curvedline.p2.left = pt.x;
curvedline.p2.top = pt.y;
var dx = curvedline.path[1][3] - pt.x;
var dy = curvedline.path[1][4] - pt.y;
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
curvedline.p2.setAngle(angle + 90);
curvedline.p2.setCoords();
}
} else if (e.target.name == "p0" || e.target.name == "p2") {
var p = e.target;
p.line1 && p.line1.set({
'x2': p.left,
'y2': p.top
});
p.line2 && p.line2.set({
'x1': p.left,
'y1': p.top
});
p.line3 && p.line3.set({
'x1': p.left,
'y1': p.top
});
p.line4 && p.line4.set({
'x1': p.left,
'y1': p.top
});
}
p && reinit();
}
function reinit() {
canvas.remove(line);
line = new fabric.Path(line.path, { fill: '',selectable: false });
line.id = line_number;
line.stroke = reinit_stroke;
canvas.add(line);
canvas.sendBackwards(line);
}
canvas.on('mouse:over', function(e) {
if(e.target.type == "path")
{
canvas.sendBackwards(e.target);
}
});
I am using following code for delete the tool.
(Note: this code runs when I click on the delete button.
Delete button deletes the object selected. )
here is the code...
var objs = canvas.getObjects();
if (activeObject) {
active_id = activeObject.get('id');
canvas.forEachObject(function (obj) {
if(obj.get('id') == active_id)
{
if(obj.type == 'path')
{
obj.selectable = true;
canvas.remove(obj);
}
canvas.remove(obj);
}
});
canvas.remove(activeObject);
}
the issue I am getting is after delete this, If I create one other curved arrow in the designer(i.e. without reload the page) and move the nodes then its still showing the old path object which we already deleted.
and If I delete the newly created curved arrow then it removes all path from canvas, In short all Path object behave like one(Hope this is understandable) .
so, the path object is not getting deleted properly it is just getting hidden when I am deleting it. Or may be it is deleted but created again when I am moving another path object..
and if we save the canvas as json then we can see the path objects which are deleted too.
What I need to do is to delete the path object properly and save just the Path object currently showing on the canvas with nodes.
Please tell me if there is some solution for this issues..
Thanks..
I found the solution for the delete curve Issue.
We need to change the code at the reinit() functions.
It was overwrite the line_id to all the line(or say path) in the canvas.
Here is the code to replace with reinit() function..
function reinit() {
var objs = canvas.getObjects();
current_line = canvas.getActiveObject();
canvas.forEachObject(function (obj) {
if(obj.type == 'path' && obj.get("id") == current_line.get("id"))
{
canvas.remove(obj);
line = new fabric.Path(obj.path, { fill: '',selectable: false });
line.id = current_line.get("id");
line.stroke = reinit_stroke;
canvas.add(line);
canvas.sendBackwards(line);
}
});
}
Hope this can be helpful for someone who need this kind of solution..
I am new in Fabric js. can anyone suggest me for restrict scale object within bounding box ?
my java-script code is below
(function(global) {
var canvas = new fabric.Canvas('maincanvas');
var yourNameObjs = [];
var goodtop =358, goodleft=250, boundingObject;
var boundingObject = new fabric.Rect({
left: 100,
top: 90,
width: 200,
height: 250,
opacity: 4,
selectable:false,
fill: 'red',
});
canvas.add(boundingObject);
addText = function(){
var nametoprint = $("#nametoprint").val();
canvas.remove(yourNameObjs);
yourNameObjs = new fabric.Text(nametoprint, {
left: 150, //Take the block's position
opacity: 9,
top: 150,
fontFamily: 'Delicious_500',
}
);
canvas.add(yourNameObjs);
canvas.on("object:moving", function(){
var obj = yourNameObjs;
var bounds = boundingObject;
obj.setCoords();
if(!obj.isContainedWithinObject(bounds)){
obj.setTop(goodtop);
obj.setLeft(goodleft);
} else {
goodtop = obj.top;
goodleft = obj.left;
}
});
canvas.on("object:scaling", function(){
var obj = yourNameObjs;
var bounds = boundingObject;
obj.setCoords();
if(!obj.isContainedWithinObject(bounds)){
obj.setTop(goodtop);
obj.setLeft(goodleft);
} else {
goodtop = obj.top;
goodleft = obj.left;
}
});
canvas.renderAll();
};
})();
html code is below
<input type="text" name="nametoprint" id="nametoprint" value="alex" />
<input type= "button" name="addtxt" id="addtxt" onclick="addText()" value="add text" />
<canvas id="maincanvas" style="border:1px solid #000;" width="400" height="450" ></canvas>
also i added this in fiddle here http://jsfiddle.net/pfgm8myp/
i want only allow to scale text object within the red bound box.moving object is working fine.i also added code for scaling but its not working.
can any one suggest me how to restrict this scaling ?
thanks in advance.
i posted it here with my own logic. http://jsfiddle.net/9xojfmyt/
i added below javascript code
(function(global) {
var canvas = new fabric.Canvas('maincanvas');
var yourNameObjs = [];
var goodtop =358, goodleft=250, boundingObject;
var boundingObject = new fabric.Rect({
left: 100,
top: 90,
width: 200,
height: 250,
opacity: 4,
selectable:false,
fill: 'red',
});
canvas.add(boundingObject);
addText = function(){
var nametoprint = $("#nametoprint").val();
canvas.remove(yourNameObjs);
yourNameObjs = new fabric.Text(nametoprint, {
left: 150, //Take the block's position
opacity: 9,
top: 150,
fontFamily: 'Delicious_500',
}
);
canvas.add(yourNameObjs);
// canvas moving limit
canvas.observe("object:moving", function(e){
var obj = yourNameObjs;
var bounds = boundingObject;
var objw = parseInt(parseInt(obj.width) * obj.scaleX);
var objh = parseInt(parseInt(obj.height) * obj.scaleY);
//left
if(obj.left < bounds.left){
obj.setLeft(bounds.left);
}
//top
if(obj.top < bounds.top){
obj.setTop(bounds.top);
}
//right
if((parseInt(obj.left) + objw) > (parseInt(bounds.left)+parseInt(bounds.width))){
obj.setLeft(((parseInt(bounds.left)+parseInt(bounds.width)) - objw));
}
//bottom
if((parseInt(obj.top) + objh) > (parseInt(bounds.top)+parseInt(bounds.height))){
obj.setTop(((parseInt(bounds.top)+parseInt(bounds.height)) - objh));
}
});
// canvas scaling limit
canvas.observe("object:scaling", function(e){
var obj = yourNameObjs;
var bounds = boundingObject;
var objw = parseInt(parseInt(obj.width) * obj.scaleX);
var objh = parseInt(parseInt(obj.height) * obj.scaleY);
//left
if(obj.left < bounds.left || (parseInt(obj.left) + objw) > (parseInt(bounds.left)+parseInt(bounds.width))){
obj.setLeft(bounds.left);
obj.setScaleX((bounds.width/obj.width));
}
//top
if(obj.top < bounds.top || (parseInt(obj.top) + objh) > (parseInt(bounds.top)+parseInt(bounds.height))){
obj.setTop(bounds.top);
obj.setScaleY((bounds.height/obj.height));
}
});
canvas.renderAll();
};
})();
Bit modification compare to Albert's answer. If you want to restrict object within canvas then replace moving event with below code.
// canvas scaling limit
canvas.observe("object:scaling", function(e){
var obj = e.target;
var bounds = {
left : 5, //here it keep 5 px margin from all direction
top : 5,
width : canvas.width - 10,
height : canvas.height - 10
};
var objw = parseInt(parseInt(obj.width) * obj.scaleX);
var objh = parseInt(parseInt(obj.height) * obj.scaleY);
//left
if(obj.left < bounds.left || (parseInt(obj.left) + objw) > (parseInt(bounds.left)+parseInt(bounds.width))){
obj.setLeft(bounds.left);
obj.setScaleX((bounds.width/obj.width));
}
//top
if(obj.top < bounds.top || (parseInt(obj.top) + objh) > (parseInt(bounds.top)+parseInt(bounds.height))){
obj.setTop(bounds.top);
obj.setScaleY((bounds.height/obj.height));
}
});
My goal is to prevent overlapping of two or more rectangles inside my FabricJS canvas.
Imagine two rectangles having info on position and size and you can drag and drop any rectangle inside the canvas.
If rectangle A gets close enough to rectangle B, the position of rectangle A should snap to the edge of rectangle B. This should work for any edge of rectangle B. The vertices do not have to match, cause the sizes of the rectangles are variable.
I have a working example for this snapping on one dimension (x-axes).
My best jsfiddle attempt
See jsfiddle.
But I need it to work around the rectangle on both dimensions. I am quite sure, that my code is not well enough to manage this.
Code-snippets which might help:
object.oCoords.tl.x //top-left corner x position. similar goes for top-right (tr), bottom-left (bl), bottom-right (br) and .y for y-position
mouse_pos = canvas.getPointer(e.e);
mouse_pos.x //pointer x.position
mouse_pos.y //pointer y.position
object.intersectsWithObject(targ) // object = dragged rectangle, targ = targeted rectangle
The snapping should work for an unlimited amount of objects (not only for two rectangles).
I solved the problem on my own.
See jsfiddle: http://jsfiddle.net/gcollect/FD53A/
This is the code:
this.canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
canvas.forEachObject(function (targ) {
var objects = this.canvas.getObjects(),
i = objects.length;
activeObject = canvas.getActiveObject();
if (targ === activeObject) return;
if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
activeObject.left = targ.left - activeObject.currentWidth;
}
if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
activeObject.left = targ.left + targ.currentWidth;
}
if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top - activeObject.currentHeight;
}
if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top + targ.currentHeight;
}
if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
targ.strokeWidth = 10;
targ.stroke = 'red';
} else {
targ.strokeWidth = 0;
targ.stroke = false;
}
if (!activeObject.intersectsWithObject(targ)) {
activeObject.strokeWidth = 0;
activeObject.stroke = false;
}
});
Works pretty legit! Cheers!
This is based on gco's answer, updated to work with FabricJS 1.5.0, with the following improvements:
Shapes don't overlap.
Snapping is more responsive.
Shapes are contained within the canvas.
JS Fiddle: https://jsfiddle.net/aphillips8/31qbr0vn/1/
var canvas = new fabric.Canvas('canvas'),
canvasWidth = document.getElementById('canvas').width,
canvasHeight = document.getElementById('canvas').height,
counter = 0,
rectLeft = 0,
snap = 20; //Pixels to snap
canvas.selection = false;
plusrect();
plusrect();
plusrect();
function plusrect(top, left, width, height, fill) {
var rect = new fabric.Rect({
top: 300,
name: 'rectangle ' + counter,
left: 0 + rectLeft,
width: 100,
height: 100,
fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
lockRotation: true,
originX: 'left',
originY: 'top',
cornerSize: 15,
hasRotatingPoint: false,
perPixelTargetFind: true,
minScaleLimit: 1,
maxWidth: canvasWidth,
maxHeight: canvasHeight
});
rect.custom = {};
rect.custom.counter = counter;
canvas.add(rect);
counter++;
rectLeft += 200;
}
function findNewPos(distX, distY, target, obj) {
// See whether to focus on X or Y axis
if(Math.abs(distX) > Math.abs(distY)) {
if (distX > 0) {
target.setLeft(obj.getLeft() - target.getWidth());
} else {
target.setLeft(obj.getLeft() + obj.getWidth());
}
} else {
if (distY > 0) {
target.setTop(obj.getTop() - target.getHeight());
} else {
target.setTop(obj.getTop() + obj.getHeight());
}
}
}
canvas.on('object:moving', function (options) {
// Sets corner position coordinates based on current angle, width and height
options.target.setCoords();
// Don't allow objects off the canvas
if(options.target.getLeft() < snap) {
options.target.setLeft(0);
}
if(options.target.getTop() < snap) {
options.target.setTop(0);
}
if((options.target.getWidth() + options.target.getLeft()) > (canvasWidth - snap)) {
options.target.setLeft(canvasWidth - options.target.getWidth());
}
if((options.target.getHeight() + options.target.getTop()) > (canvasHeight - snap)) {
options.target.setTop(canvasHeight - options.target.getHeight());
}
// Loop through objects
canvas.forEachObject(function (obj) {
if (obj === options.target) return;
// If objects intersect
if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {
var distX = ((obj.getLeft() + obj.getWidth()) / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
var distY = ((obj.getTop() + obj.getHeight()) / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);
// Set new position
findNewPos(distX, distY, options.target, obj);
}
// Snap objects to each other horizontally
// If bottom points are on same Y axis
if(Math.abs((options.target.getTop() + options.target.getHeight()) - (obj.getTop() + obj.getHeight())) < snap) {
// Snap target BL to object BR
if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth());
options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
}
// Snap target BR to object BL
if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
options.target.setLeft(obj.getLeft() - options.target.getWidth());
options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
}
}
// If top points are on same Y axis
if(Math.abs(options.target.getTop() - obj.getTop()) < snap) {
// Snap target TL to object TR
if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth());
options.target.setTop(obj.getTop());
}
// Snap target TR to object TL
if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
options.target.setLeft(obj.getLeft() - options.target.getWidth());
options.target.setTop(obj.getTop());
}
}
// Snap objects to each other vertically
// If right points are on same X axis
if(Math.abs((options.target.getLeft() + options.target.getWidth()) - (obj.getLeft() + obj.getWidth())) < snap) {
// Snap target TR to object BR
if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
options.target.setTop(obj.getTop() + obj.getHeight());
}
// Snap target BR to object TR
if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
options.target.setTop(obj.getTop() - options.target.getHeight());
}
}
// If left points are on same X axis
if(Math.abs(options.target.getLeft() - obj.getLeft()) < snap) {
// Snap target TL to object BL
if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
options.target.setLeft(obj.getLeft());
options.target.setTop(obj.getTop() + obj.getHeight());
}
// Snap target BL to object TL
if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
options.target.setLeft(obj.getLeft());
options.target.setTop(obj.getTop() - options.target.getHeight());
}
}
});
options.target.setCoords();
// If objects still overlap
var outerAreaLeft = null,
outerAreaTop = null,
outerAreaRight = null,
outerAreaBottom = null;
canvas.forEachObject(function (obj) {
if (obj === options.target) return;
if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {
var intersectLeft = null,
intersectTop = null,
intersectWidth = null,
intersectHeight = null,
intersectSize = null,
targetLeft = options.target.getLeft(),
targetRight = targetLeft + options.target.getWidth(),
targetTop = options.target.getTop(),
targetBottom = targetTop + options.target.getHeight(),
objectLeft = obj.getLeft(),
objectRight = objectLeft + obj.getWidth(),
objectTop = obj.getTop(),
objectBottom = objectTop + obj.getHeight();
// Find intersect information for X axis
if(targetLeft >= objectLeft && targetLeft <= objectRight) {
intersectLeft = targetLeft;
intersectWidth = obj.getWidth() - (intersectLeft - objectLeft);
} else if(objectLeft >= targetLeft && objectLeft <= targetRight) {
intersectLeft = objectLeft;
intersectWidth = options.target.getWidth() - (intersectLeft - targetLeft);
}
// Find intersect information for Y axis
if(targetTop >= objectTop && targetTop <= objectBottom) {
intersectTop = targetTop;
intersectHeight = obj.getHeight() - (intersectTop - objectTop);
} else if(objectTop >= targetTop && objectTop <= targetBottom) {
intersectTop = objectTop;
intersectHeight = options.target.getHeight() - (intersectTop - targetTop);
}
// Find intersect size (this will be 0 if objects are touching but not overlapping)
if(intersectWidth > 0 && intersectHeight > 0) {
intersectSize = intersectWidth * intersectHeight;
}
// Set outer snapping area
if(obj.getLeft() < outerAreaLeft || outerAreaLeft == null) {
outerAreaLeft = obj.getLeft();
}
if(obj.getTop() < outerAreaTop || outerAreaTop == null) {
outerAreaTop = obj.getTop();
}
if((obj.getLeft() + obj.getWidth()) > outerAreaRight || outerAreaRight == null) {
outerAreaRight = obj.getLeft() + obj.getWidth();
}
if((obj.getTop() + obj.getHeight()) > outerAreaBottom || outerAreaBottom == null) {
outerAreaBottom = obj.getTop() + obj.getHeight();
}
// If objects are intersecting, reposition outside all shapes which touch
if(intersectSize) {
var distX = (outerAreaRight / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
var distY = (outerAreaBottom / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);
// Set new position
findNewPos(distX, distY, options.target, obj);
}
}
});
});
I based this fiddle off #Anna Phillips' and #gco's examples. It includes:
Corner snapping
Edge snapping
Objects can overlap
Objects are fully contained within the canvas
Objects cannot have a size larger than the canvas area
Here is the code:
window.canvas = new fabric.Canvas('fabriccanvas');
window.counter = 0;
var newleft = 0,
edgedetection = 20, //pixels to snap
canvasWidth = document.getElementById('fabriccanvas').width,
canvasHeight = document.getElementById('fabriccanvas').height;
canvas.selection = false;
plusrect();
plusrect();
plusrect();
function plusrect(top, left, width, height, fill) {
window.canvas.add(new fabric.Rect({
top: 300,
name: 'rectangle ' + window.counter,
left: 0 + newleft,
width: 100,
height: 100,
fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
lockRotation: true,
originX: 'left',
originY: 'top',
cornerSize: 15,
hasRotatingPoint: false,
perPixelTargetFind: true,
minScaleLimit: 1,
maxHeight: document.getElementById("fabriccanvas").height,
maxWidth: document.getElementById("fabriccanvas").width,
}));
window.counter++;
newleft += 200;
}
this.canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
if(obj.getLeft() < edgedetection) {
obj.setLeft(0);
}
if(obj.getTop() < edgedetection) {
obj.setTop(0);
}
if((obj.getWidth() + obj.getLeft()) > (canvasWidth - edgedetection)) {
obj.setLeft(canvasWidth - obj.getWidth());
}
if((obj.getHeight() + obj.getTop()) > (canvasHeight - edgedetection)) {
obj.setTop(canvasHeight - obj.getHeight());
}
canvas.forEachObject(function (targ) {
activeObject = canvas.getActiveObject();
if (targ === activeObject) return;
if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
activeObject.left = targ.left - activeObject.currentWidth;
}
if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
activeObject.left = targ.left + targ.currentWidth;
}
if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top - activeObject.currentHeight;
}
if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top + targ.currentHeight;
}
if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
targ.strokeWidth = 10;
targ.stroke = 'red';
} else {
targ.strokeWidth = 0;
targ.stroke = false;
}
if (!activeObject.intersectsWithObject(targ)) {
activeObject.strokeWidth = 0;
activeObject.stroke = false;
}
});
});
What I'd like to know is if it's possible to extend this to add the following features:
Dynamic snapping. Continuing to drag an object after the initial snap will temporarily disable snapping until the object stops moving. For example, if I drag one box next to another, they will snap together once they are within range. However if I continue moving the first box, I can "drop" it in a position where it is within the snapping range but not aligned to the other box.
Show guide lines when selected object is within range of another object. Currently we add a border around the target object, but it would be better to show guidelines that extend outwards (possibly to the edge of the canvas) to more easily visualize the bounds of the target object.
Parallel snapping. When moving an object that is already snapped to the target object, the selected object should snap to the target object in such a way that the tops, bottoms, or sides of both objects are parallel. For example, assume that the selected square is snapped to the left of the target square and that the top of the selected square is below the top of the target square. Moving the selected square up should cause its top to snap into alignment with the top of the target once in range. The same logic should apply when moving it down, or if the selected object is above/below the target and being moves horizontally.
I needed snapping of unequal sized areas. jsfiddle
var canvas = new fabric.Canvas('c');
canvas.setDimensions({width:window.innerWidth});
var edge_detection_external = 21;
var corner_detection = 5;
canvas.selection = false;
canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords();
function update_position(obj){
return function(targ){
if(targ === obj) return;
// Check overlap case https://www.geeksforgeeks.org/find-two-rectangles-overlap/
if(!(function(targ,obj){
if(obj.aCoords.tl.x > targ.aCoords.br.x || targ.aCoords.tl.x > obj.aCoords.br.x)
return false;
if(targ.aCoords.tl.y > obj.aCoords.br.y || obj.aCoords.tl.y > targ.aCoords.br.y)
return false;
return true;
})(targ,obj)){
// is on RIGHT or LEFT?
if((obj.top > targ.top && obj.top < targ.top + targ.height)
|| (targ.top > obj.top && targ.top < obj.top + obj.height)){
// Object is to the RIGHT and Edge detection
if(obj.aCoords.tl.x > targ.aCoords.br.x
&& obj.aCoords.tl.x - targ.aCoords.br.x < edge_detection_external){
obj.set({left:targ.aCoords.br.x});
// Corner detection
obj.setCoords();
if(Math.abs(targ.aCoords.tr.y - obj.aCoords.tl.y) < corner_detection)
obj.set({top:targ.top});
else if(Math.abs(targ.aCoords.br.y - obj.aCoords.bl.y) < corner_detection)
obj.set({top:targ.top + targ.height - obj.height});
}
// LEFT
if(targ.aCoords.tl.x > obj.aCoords.br.x
&& targ.aCoords.tl.x - obj.aCoords.br.x < edge_detection_external){
obj.set({left:targ.aCoords.tl.x - obj.width});
obj.setCoords();
if(Math.abs(targ.aCoords.tl.y - obj.aCoords.tr.y) < corner_detection)
obj.set({top:targ.top});
else if(Math.abs(targ.aCoords.bl.y - obj.aCoords.br.y) < corner_detection)
obj.set({top:targ.top + targ.height - obj.height});
}
}
// is on TOP or BOTTOM?
if((obj.left > targ.left && obj.left < targ.left + targ.width)
|| (targ.left > obj.left && targ.left < obj.left + obj.width)){
// TOP
if(targ.aCoords.tl.y > obj.aCoords.br.y
&& targ.aCoords.tl.y - obj.aCoords.br.y < edge_detection_external){
obj.set({top:targ.aCoords.tl.y - obj.height});
obj.setCoords();
if(Math.abs(targ.aCoords.tl.x - obj.aCoords.bl.x) < corner_detection)
obj.set({left:targ.left});
else if(Math.abs(targ.aCoords.tr.x - obj.aCoords.br.x) < corner_detection)
obj.set({left:targ.left + targ.width - obj.width});
}
// BOTTOM
if(obj.aCoords.tl.y > targ.aCoords.br.y
&& obj.aCoords.tl.y - targ.aCoords.br.y < edge_detection_external){
obj.set({top:targ.aCoords.br.y});
obj.setCoords();
if(Math.abs(targ.aCoords.bl.x - obj.aCoords.tl.x) < corner_detection)
obj.set({left:targ.left});
else if(Math.abs(targ.aCoords.br.x - obj.aCoords.tr.x) < corner_detection)
obj.set({left:targ.left + targ.width - obj.width});
}
}
}
}
}
canvas.getObjects('group').some(update_position(obj));
});
String.prototype.to_inches = function(){
return this.split('-').map(function(value,index){
value = Number(value);
if(index == 0)
return value * 12
else
return value
}).reduce(function(total,current){
return total + current;
});
}
Array.prototype.to_object_list = function(){
const preserved = [...this];
var header = this.splice(0,1)[0];
for(var i = 0;i < this.length; i++){
var obj = {};
for(var j = 0;j < header.length; j++){
obj[header[j].toLowerCase()] = this[i][j];
}
this[i] = obj;
}
return preserved;
}
function draw_areas(){
var offset = 0;
return function(area_params,index){
if(area_params.area.indexOf('>') === -1){
var area = new fabric.Rect({
fill: 'red',
width:area_params.width.to_inches(),
height:area_params.length.to_inches(),
});
var text = new fabric.Text(area_params.area + '\n' + area_params.width + ' x ' + area_params.length,{
fontSize:12,
fill:"white"
});
if(text.width - area.width > 0){
text.set('width',area.width);
}
if(text.height - area.height > 0){
text.set('height',area.height);
}
var group_name = 'group_' + area_params.area.split(' ').join('-');
var group = new fabric.Group([area,text],{
name: group_name,
left: 5,
top: 5*(index++) + offset,
});
canvas.add(group);
offset = area_params.length.to_inches() + offset;
canvas.setDimensions({height:5*(index++) + offset});
}
}
}
function handler_get_data(data){
data = JSON.parse(data);
data.to_object_list();
data.forEach(draw_areas());
}
var d = '[["Area","Width","Length"],["Bedroom 1","19-5.5","14"],["Kitchen","14","16-3"],["Bedroom 2","13-6","12-9"]]';
handler_get_data(d);