I am trying to serialize/deserialize a custom farbicJS object that is subclassed from fabric.Textbox, but when loading the object back into the canvas from the saved JSON, the custom object gets restored with the wrong attributes, and it no longer behaves the way that it should when resizing the object.
Here is my custom object that is used to create a textbox with padding, and fromObject method I tried to use below:
EDITED: I fixed the initial height issue by refactoring the toObject method to include _renderBackground and padding
fabric.TextboxWithPadding = fabric.util.createClass(fabric.Textbox, {
type: 'TextboxWithPadding',
_renderBackground: function (ctx) {
if (!this.backgroundColor) {
return;
}
var dim = this._getNonTransformedDimensions();
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(
-dim.x / 2 - this.padding,
-dim.y / 2 - this.padding,
dim.x + this.padding * 2,
dim.y + this.padding * 2
);
this._removeShadow(ctx);
},
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {
_renderBackground: this._renderBackground,
padding: this.padding
});
},
});
fabric.TextboxWithPadding.fromObject = function (object, callback) {
return fabric.Object._fromObject("TextboxWithPadding", object, callback, 'text');
};
When adding the TextboxWithPadding I am using the following:
if (drawingObject.type === "label") {
canvas.selection = false;
var pointer = canvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
label = new fabric.TextboxWithPadding('New Label', {
selectable: true,
fill: '#555555',
left: origX,
top: origY,
lockRotation: true,
fontSize: 18,
fontWeight: 400,
backgroundColor: '#ffffff',
width: 100,
height: 100,
textAlign: 'center',
fontFamily: 'Open Sans',
padding: 10
})
label.setControlsVisibility({
mt: false,
bl: false,
br: false,
tl: false,
tr: false,
mb: true,
});
canvas.add(label);
let lastHeight;
let calcFont;
const updateTextSize = () => {
const controlPoint = label.__corner;
//mr and ml are the only controlPoints that don't modify textbox height
if (controlPoint && controlPoint != "mr" && controlPoint != "ml") {
lastHeight = label.height * label.scaleY;
calcFont = lastHeight * .8
if (calcFont < 10) {
calcFont = 10
}
} else {
lastHeight = label.height
calcFont = label.fontSize
}
label.set({
height: lastHeight || label.height,
scaleY: 1,
scaleX: 1,
fontSize: calcFont
});
canvas.renderAll();
};
label.on('scaling', updateTextSize);
// Handle textbox content updates
canvas.on('text:changed', function () {
label.set('width', label.width)
$(".deleteBtn").remove();
});
canvas.on('mouse:up', function (options) {
if (drawingObject.type === "label") {
drawingObject.type = ''
}
canvas.renderAll();
canvas.selection = true;
});
}
Here is the result of saving this object to JSON (excluding the backgroundImage data url):
{"version":"2.4.0","objects":[{"type":"TextboxWithPadding","version":"2.4.0","originX":"left","originY":"top","left":49.5,"top":49,"width":100,"height":20.34,"fill":"#555555","stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":4,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"#ffffff","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"text":"New Label","fontSize":18,"fontWeight":400,"fontFamily":"Open Sans","fontStyle":"normal","lineHeight":1.16,"underline":false,"overline":false,"linethrough":false,"textAlign":"center","textBackgroundColor":"","charSpacing":0,"minWidth":20,"styles":{}}]}
You can see how the height changes from the default 100 to 20.34 as well. I'm thinking that the event handlers need to be added to the custom object when it is loaded back into the canvas, but I'm not entirely sure.
Any help with this is greatly appreciated, thank you.
Alright, after messing around with this some more, I figured it out.
I needed to add in my custom methods and properties to toObject class method like so:
// ...
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {
_renderBackground: this._renderBackground,
padding: this.padding,
});
},
// ...
and to add the listeners back to the restored object I added a 'reviver function' as the thrid argument to canvas.loadFromJSON which allows me to access the objects added to the canvas from JSON:
$("#load-from-json").click(function () {
canvas.loadFromJSON(
JSON.parse(canvasSavedJSON),
canvas.renderAll.bind(canvas),
function (o, object) {
if (object.type == 'TextboxWithPadding') {
object.setControlsVisibility({
mt: false,
bl: false,
br: false,
tl: false,
tr: false,
mb: true,
});
let lastHeight;
let calcFont;
const updateTextSize = () => {
const controlPoint = object.__corner;
//mr and ml are the only controlPoints that dont modify textbox height
if (controlPoint && controlPoint != "mr" && controlPoint != "ml") {
lastHeight = object.height * object.scaleY;
calcFont = lastHeight * .8
if (calcFont < 10) {
calcFont = 10
}
} else {
lastHeight = object.height
calcFont = object.fontSize
}
object.set({
height: lastHeight || object.height,
scaleY: 1,
scaleX: 1,
//padding: lastHeight,
fontSize: calcFont
});
canvas.renderAll();
};
object.on('scaling', updateTextSize);
}
}
);
});
Related
I'm trying to check for a collision between 2 text objects and using the intersectsWithObject. It's working but it's taking the bouding rect into account. Is it possible to check on pixel level?
Current behaviour:
Wanted behaviour:
const canvas = new fabric.Canvas('canvas');
canvas.setWidth(document.body.clientWidth);
canvas.setHeight(document.body.clientHeight);
const text = new fabric.Textbox('some text', {
width: 300,
fontSize: 70,
top: 120,
left: 100
});
const text2 = new fabric.Textbox('some more text', {
width: 350,
fontSize: 50,
top: 200,
left: 20,
})
if (text.intersectsWithObject(text2, true, true)) {
text.set('fill', 'red');
}
else {
text.set('fill', 'black');
}
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = '#555';
canvas.forEachObject(function(obj) {
var bound = obj.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
})
});
canvas.add(text);
canvas.add(text2);
https://jsbin.com/menadejato/edit?js,console,output
I am new to Fabric and I have created a Rectangular shape and clipTo function for image and now i want to restrict the image in shape area. Top and left is working fine but right and bottom is not smooth. please help
https://jsfiddle.net/mg73n0eq/154/#&togetherjs=Qh6LnTRHFq
let canvas = new fabric.Canvas(
"ast",
{
width: 400,
height: 400,
selection: true,
id: "ast",
//selectionBorderColor: "green",
backgroundColor: "#ffffff",
preserveObjectStacking: true,
uniScaleTransform: true,
//cornerSize: 40,
lockScalingFlip: true,
controlsAboveOverlay: true,
subTargetCheck: true,
hoverCursor: "pointer",
stateful: true,
},
null,
"anonymous"
);
var Rect = new fabric.Rect({
width: 250,
height: 250,
fixed: true,
fill: "#000",
scaleX: 1,
scaleY: 1,
top:0,
left:0,
maskname: "rect",
absolutePositioned: true,
stroke: "1",
lockScalingFlip: true,
lockUniScaling: true,
minScaleLimit: 0.1,
});
canvas.add(Rect);
function rectMask(canvas) {
//Rect.viewportCenter();
let pugImg = new Image();
pugImg.crossOrigin = "anonymous";
pugImg.src ="https://posterapplab.com/api/images/photos/ZaJprCvDjVA.png";
//console.log(size.zoom);
pugImg.onload = function (img) {
let pug = new fabric.Image(pugImg, {
crossOrigin: "anonymous",
scaleX:0.3,
scaleY:0.3,
top: 20,
left: 10,
maskname: "rect",
clipPath: Rect,
clipTo: function (ctx) {
clipMyObject(this, ctx);
},
});
pug.crossOrigin = "anonymous";
pug.backgroundVpt = false;
pug.setControlsVisibility({
mt: false,
mb: false,
});
canvas.add(pug);
canvas.renderAll();
};
}
function clipMyObject(thisObj, ctx) {
if (thisObj.clipPath) {
ctx.save();
if (thisObj.clipPath.fixed) {
var retina = thisObj.canvas.getRetinaScaling();
ctx.setTransform(retina, 0, 0, retina, 0, 0);
// to handle zoom
ctx.transform.apply(ctx, thisObj.canvas.viewportTransform);
thisObj.clipPath.transform(ctx);
}
thisObj.clipPath._render(ctx);
ctx.restore();
ctx.clip();
var x = -thisObj.width / 2,
y = -thisObj.height / 2,
elementToDraw;
if (
thisObj.isMoving === false &&
thisObj.resizeFilter &&
thisObj._needsResize()
) {
thisObj._lastScaleX = thisObj.scaleX;
thisObj._lastScaleY = thisObj.scaleY;
thisObj.applyResizeFilters();
}
elementToDraw = thisObj._element;
elementToDraw &&
ctx.drawImage(
elementToDraw,
0,
0,
thisObj.width,
thisObj.height,
x,
y,
thisObj.width,
thisObj.height
);
thisObj._stroke(ctx);
thisObj._renderStroke(ctx);
}
}
rectMask(canvas);
canvas.on("object:moving", (e) => {
if (
e.target &&
e.target.type === "image" &&
e.target.maskname
) {
//obj.canvas.lastScaleY = obj.scaleY;
//obj.canvas.lastScaleX = obj.scaleX;
let bound = Rect.getBoundingRect();
let selectedBound = e.target.getBoundingRect();
//selectedObject.set("left", 0);
//console.log(bound.width);
// console.log(bound.height);
if (e.target.left > Rect.left) {
e.target.set("left", Rect.left);
}
if (e.target.top > Rect.top) {
e.target.set("top", Rect.top);
}
var right = Rect.left + Rect.getBoundingRect().width / 2;
var bottom = Rect.top + Rect.getBoundingRect().height / 2;
if (right > e.target.left + e.target.width / 2) {
//Solution one
e.target.set("left", Rect.left - Rect.width);
}
if (bottom > e.target.top + e.target.height / 2) {
e.target.set("top", Rect.top - Rect.height / 2);
}
// console.log(selectedObject);
/*if (selectedBound.left + selectedBound.width < bin.width) {
console.log("hjfhsafdhagsdf");
selectedObject.set("left", bin.left - bin.width / 2);
}*/
}
});
I am new to Fabric and I have created a Rectangular shape and clipTo function for image and now i want to restrict the image in shape area. Top and left is working fine but right and bottom is not smooth. please help
How can you create a checkbox like component with Fabric JS? More like to click on the image, and to set the opacity to 0. If you click again, to set the opacity to 1.
Here is my code:
For the checkbox image:
fabric.Image.fromURL('https://image.flaticon.com/icons/svg/33/33281.svg', (image) => {
image.scale(0.35);
image.set({
left: 152,
top: 120,
hoverCursor: 'default',
selectable: true,
opacity: 0.5,
hasControls: false,
lockMovementX: true,
lockMovementY: true
})
if(image.onselect === true ) {
image.set().opacity = 0;
}
canvas.add(image);
});
The box where the image is put:
function addBox(left, top, width, height) {
const o = new fabric.Rect({
left: left,
top: top,
width: 30,
height: 30,
fill: boxFill,
strokeWidth: 2,
originX: 'left',
originY: 'top',
centeredRotation: true,
snapAngle: 45,
selectable: true,
type: 'box',
id: generateId()
})
if(image.onselect === true ) {
image.set().opacity = 0;
}
canvas.add(o)
canvas.getObjects().map(o => {
o.hasControls = false
o.lockMovementX = true
o.lockMovementY = true
o.borderColor = '#38A62E'
o.borderScaleFactor = 2.5
})
canvas.selection = false
canvas.hoverCursor = 'pointer'
canvas.discardActiveObject()
canvas.renderAll()
return o
}
How can I resolve this issue? Is there a way to make the image appear and dissapear when you click on it ( using opacity or something similarly) ?
Hope this helps you and many who want to add Event on objects.
var canvas = new fabric.Canvas("canvas");
function drawCheckbox(left,right, width, height){
var imgClass = new fabric.Image.fromURL('https://image.flaticon.com/icons/svg/33/33281.svg',function(img){
img.width = width;
img.height = height;
img.left = left;
img.top = right;
img.hasControls = false
img.on('mousedown', function(e) {
if(e.target.opacity <= 0.5){
e.target.opacity = 1;
}else{
e.target.opacity = 0.4;
}
canvas.renderAll();
});
canvas.add(img);
canvas.renderAll();
})
}
drawCheckbox(0,0, 100,100)
drawCheckbox(100,100, 100,100)
drawCheckbox(200,200, 100,100)
body {
background-color:silver;
}
canvas {
border:1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"> </script>
<canvas id="canvas" width=300 height=300></canvas><br>
I've created a fabric custom class "VectorPlaceholder" that is basically a group that contains a Rectangle and a Vector:
// Fabric.js custom vector EPS object
fabric.VectorPlaceholder = fabric.util.createClass(fabric.Group, {
async: true,
type: 'vector-placeholder',
lockUniScalingWithSkew: false,
noScaleCache: true,
initialize: function (options) {
boundsRectangle = new fabric.Rect({
strokeDashArray: [10,10],
originX: 'center',
originY: 'center',
stroke: '#000000',
strokeWidth: 1,
width: options.width || 300,
height: options.height || 300,
fill: 'rgba(0, 0, 0, 0)',
});
this.setControlsVisibility({
ml: false,
mb: false,
mr: false,
mt: false,
});
this.originX = 'center',
this.originY = 'center',
this.callSuper('initialize', [boundsRectangle], options);
},
setVector: function (vector) {
//We remove any EPS that was in that position
var EPSGroup = this;
EPSGroup.forEachObject(function (object) {
if (object && object.type != "rect") {
EPSGroup.remove(object);
}
});
var scale = 1;
var xOffset = EPSGroup.getScaledWidth() / 2;
var yOffset = EPSGroup.getScaledHeight() / 2;
if (vector.height > vector.width) {
scale = EPSGroup.getScaledHeight() / vector.height;
xOffset = xOffset - (EPSGroup.getScaledWidth() - vector.width * scale) / 2
}
else {
scale = EPSGroup.getScaledWidth() / vector.width;
yOffset = yOffset - (EPSGroup.getScaledHeight() - vector.height * scale) / 2
}
vector.left = EPSGroup.left - xOffset;
vector.top = EPSGroup.top - yOffset;
vector.set('scaleY', scale);
vector.set('scaleX', scale);
var angle = 0;
if (EPSGroup.get('angle')) {
angle = EPSGroup.get('angle');
vector.setAngle(angle);
}
EPSGroup.addWithUpdate(vector);
EPSGroup.setCoords();
},
});
The idea of this class is to have a placeholder where users can upload SVGs.
This is done by calling to fabric.loadSVGFromString and then passing the result to the function in my custom class (setVector)
fabric.loadSVGFromString(svgString, function(objects, options) {
// Group the SVG objects to make a single element
var a = fabric.util.groupSVGElements(objects, options);
var EPSGroup = new fabric.VectorPlaceholder({});
EPSGroup.setVector(a);
This works perfectly when I create my custom object and don't rotate it. As you can see the group controls are aligned with the dashed rectangle.
The problem is when I create an empty VectorPlaceholder and I rotate it manually. After the manual rotation, when setVector is called this is what happens:
I can't understand why the group controls ignore the rotation, what I'm doing wrong? How can I make the group controls render aligned with the rotated rectangle?
You need to set the angle after you make setVector method
http://jsfiddle.net/2segrwx0/1/
// Fabric.js custom vector EPS object
fabric.VectorPlaceholder = fabric.util.createClass(fabric.Group, {
async: true,
type: 'vector-placeholder',
lockUniScalingWithSkew: false,
noScaleCache: true,
initialize: function (options) {
boundsRectangle = new fabric.Rect({
strokeDashArray: [10,10],
originX: 'center',
originY: 'center',
stroke: '#000000',
strokeWidth: 1,
width: options.width || 300,
height: options.height || 300,
fill: 'rgba(0, 0, 0, 0)',
});
this.setControlsVisibility({
ml: false,
mb: false,
mr: false,
mt: false,
});
this.originX = 'center',
this.originY = 'center',
this.callSuper('initialize', [boundsRectangle], options);
},
setVector: function (vector) {
//We remove any EPS that was in that position
var EPSGroup = this;
EPSGroup.forEachObject(function (object) {
if (object && object.type != "rect") {
EPSGroup.remove(object);
}
});
var scale = 1;
var xOffset = EPSGroup.getScaledWidth() / 2;
var yOffset = EPSGroup.getScaledHeight() / 2;
if (vector.height > vector.width) {
scale = EPSGroup.getScaledHeight() / vector.height;
xOffset = xOffset - (EPSGroup.getScaledWidth() - vector.width * scale) / 2
}
else {
scale = EPSGroup.getScaledWidth() / vector.width;
yOffset = yOffset - (EPSGroup.getScaledHeight() - vector.height * scale) / 2
}
vector.left = EPSGroup.left - xOffset;
vector.top = EPSGroup.top - yOffset;
vector.set('scaleY', scale);
vector.set('scaleX', scale);
/*var angle = 0;
if (EPSGroup.get('angle')) {
angle = EPSGroup.get('angle');
vector.setAngle(angle);
} */
EPSGroup.addWithUpdate(vector);
EPSGroup.setCoords();
},
});
canvas = new fabric.Canvas('c', {
});
fabric.loadSVGFromString('<svg height="210" width="500"><polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:lime;stroke:purple;stroke-width:5;fill-rule:nonzero;" /></svg>', function(objects, options) {
// Group the SVG objects to make a single element
var a = fabric.util.groupSVGElements(objects, options);
var EPSGroup = new fabric.VectorPlaceholder({});
EPSGroup.left=200;
EPSGroup.top=200;
EPSGroup.setVector(a);
EPSGroup.angle=45;
canvas.add(EPSGroup);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.5/fabric.js"></script>
<canvas id="c" width=500 height=300 ></canvas>
I am working at a project with fabric js. I tried to minimize my problem, so I'm hoping that the code isn't too messed up.
I am creating some Objects which are linked with each other:
A Line, which contains a Start and an Endpoint
A Circle, which is StartPoint of 1 line and Endpoint of another line
with this combination i can create different shapes(like a polygon) and modify my move-functions for them too.
When a Circle is dragged, the related Lines are scaling and moving too. (in my code you can move the lines too and the shape is resized after that, but i didnt put it into this example, bc this short extract should be enough to show what my problem is.)
I got a little example in jsfiddle: https://jsfiddle.net/bxgox7cr/
When you look at the ends of the lines, you can clearly see a cut, so the eye soon recognize, that this is not a connected shape but rather some lines which are close to each other. Is there a way to modify the look of the lines, that the shape looks "closed"?
Here is my code, i tried to put some comments, that it is easy to read:
var canvas = new fabric.Canvas('canvas');
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
document.getElementById("canvas").tabIndex = 1000;
/** ------------creating a Line Object, which contains a start and an endpoint ------------**/
fabric.LineWithPoints = fabric.util.createClass(fabric.Line, {
initialize: function(points, options) {
options || (options = {});
this.callSuper('initialize', points, options);
options &&
this.set('type', options.type),
this.set('name', options.name),
this.set('start_point', options.start_point),
this.set('end_point', options.end_point),
this.set('current_x', options.current_x),
this.set('current_y', options.current_y)
},
setStartPointAndEndPoint: function(start_point, end_point) {
this.set({
start_point: start_point,
end_point: end_point
});
},
setValues: function(new_x1, new_y1, new_x2, new_y2) {
// console.log(this);
this.set({
x1: new_x1,
x2: new_x2,
y1: new_y1,
y2: new_y2
});
this.setCoords();
}
});
/**--- modifie the circle element, adding new functions for the movement of the object-------*/
fabric.LinePoint = fabric.util.createClass(fabric.Circle, {
initialize: function(options) {
options || (options = {});
this.callSuper('initialize', options);
options &&
this.set('subtype', 'line_point'),
this.set('x', this.left),
this.set('y', this.top)
},
setPointCoordinates: function(new_left, new_top) {
this.set({
x: new_left,
y: new_top,
left: new_left,
top: new_top
});
this.setCoords();
},
move: function(new_left, new_top) {
var wall_1 = this.line1;
var wall_2 = this.line2;
this.setPointCoordinates(new_left, new_top);
wall_1.setValues(wall_1.x1, wall_1.y1, this.getLeft(), this.getTop());
wall_2.setValues(this.getLeft(), this.getTop(), wall_2.x2, wall_2.y2);
canvas.renderAll();
},
});
/**------------------- Moving Function------------------------------------------------- */
canvas.on('object:moving', function(event) {
var object = event.target;
if (object.subtype == "line_point") {
object.move(object.getLeft(), object.getTop());
}
});
/**------------------------------ create functions for the objects -----------------------*/
function newCircleObject(left, top, wall_1, wall_2) {
var circle = new fabric.LinePoint({
left: left,
top: top,
strokeWidth: 2,
radius: 15,
fill: 'grey',
stroke: 'black',
opacity: 0.1,
perPixelTargetFind: true,
subtype: 'line_point',
includeDefaultValues: false
});
circle.hasControls = false;
circle.hasBorders = false;
circle.line1 = wall_1;
circle.line2 = wall_2;
return circle;
}
function newWallObject(coords) {
var wall = new fabric.LineWithPoints(coords, {
stroke: 'black',
strokeWidth: 6,
lockScalingX: true,
lockScalingY: true,
perPixelTargetFind: true,
subtype: 'line',
type: 'line',
padding: 10,
includeDefaultValues: false
});
wall.hasControls = false;
wall.hasBorders = false;
return wall;
}
/**------------------------------ adding the shapes--------------------------------*/
var wall_1 = newWallObject([100, 100, 100, 500]);
var wall_2 = newWallObject([100, 500, 500, 500]);
var wall_3 = newWallObject([500, 500, 500, 100]);
var wall_4 = newWallObject([500, 100, 100, 100]);
var end_point_1 = newCircleObject(wall_1.x1, wall_1.y1, wall_4, wall_1);
var end_point_2 = newCircleObject(wall_2.x1, wall_2.y1, wall_1, wall_2);
var end_point_3 = newCircleObject(wall_3.x1, wall_3.y1, wall_2, wall_3);
var end_point_4 = newCircleObject(wall_4.x1, wall_4.y1, wall_3, wall_4);
wall_1.setStartPointAndEndPoint(end_point_1.name, end_point_2.name);
wall_2.setStartPointAndEndPoint(end_point_2.name, end_point_3.name);
wall_3.setStartPointAndEndPoint(end_point_3.name, end_point_4.name);
wall_4.setStartPointAndEndPoint(end_point_4.name, end_point_1.name);
canvas.add(wall_1, wall_2, wall_3, wall_4, end_point_1, end_point_2, end_point_3, end_point_4);
Add strokeLineCap: 'round',:
function newWallObject(coords) {
var wall = new fabric.LineWithPoints(coords, {
stroke: 'black',
strokeWidth: 6,
lockScalingX: true,
lockScalingY: true,
perPixelTargetFind: true,
strokeLineCap: 'round',
subtype: 'line',
type: 'line',
padding: 10,
includeDefaultValues: false
});
wall.hasControls = false;
wall.hasBorders = false;
return wall;
}
I looked up: http://fabricjs.com/docs/fabric.Object.html#strokeLineCap