FabricJS iText Width And Height - fabricjs

Is there a way to set the width and height of an iText object? When creating the object if you set width and height it doesn't do anything. What I am after is the bounding box to be a fixed size on the canvas as an editable region, I have already extended the iText class to allow for individual character editing but I can't seem to work out how to size the box as a fixed size and allow content editing inside of it. Bare in mind that the text box can't be moved or scaled, it's static.

It's Too late for answer but other may be looking for answer.
First of all You can use TextBox class of fabricjs if you want to give fixed width & height to Text field. It is Subclass of Itext. But the problem with TextBox was it's text wrap.
So If you don't want to use TextBox but still want to have fixed width and height with Itext then following extended subclass can help you.
I have overridden the initDimensions method .
this.LimitedTextbox = fabric.util.createClass(fabric.IText, {
initDimensions: function() {
this.isEditing && this.initDelayedCursor();
this.clearContextTop();
if (this.__skipDimension) {
return;
}
this._splitText();
this._clearCache();
if (this.path) {
this.width = this.path.width;
this.height = this.path.height;
}
else {
let width = this.calcTextWidth();
if(width>this.maxWidth){
this.width = width;
}else{
this.width = this.maxWidth;
}
this.height = this.calcTextHeight();
}
if (this.textAlign.indexOf('justify') !== -1) {
this.enlargeSpaces();
}
this.saveState({ propertySet: '_dimensionAffectingProps' });
},
onKeyDown:function(e){
if (e.keyCode == 13 && this._textLines.length>=this.maxLines) {
this.exitEditing();
}
this.callSuper('onKeyDown',e);
let maxLine = Math.max(...this.__lineWidths);
self.changeBorderWidth(this.toObject().groupId, this, { iWidth:
this.width, iHeight: this.height,maxLine:maxLine,id:this.toObject().id
},false,this.toObject().customType==='stamp_input');
if(this.dynamicMinWidth>this.maxWidth){
this.width = this.dynamicMinWidth;
}
},
});
Then you can use like below
const text = new this.LimitedTextbox(txt, {
left: 100,
top: 100,
fontFamily: "arial",
angle: 0,
fontSize: 24,
fontWeight: "bold",
hasControls: true,
hoverCursor: "text",
width:250,
maxWidth: 250,
maxLines: 3,
hasRotatingPoint: true,
});
I Hope it helps , you can modify initDimensions as per your requirement , you can also see other methods in fabric's documemtation.

Related

Fabricjs - selection only via border

I'm using Fabric.js to draw some rectangles on a canvas. The default behavior is that clicking inside a rectangle selects it. How can I change the behavior such that it is only selected when clicking on the border of the rectangle?
Clicking inside the rectangle but not on the border should do nothing.
You can see this behavior by drawing a rectangle on a TradingView.com chart
It there an option for this in fabric, and if not how could I go around implementing it?
This approach overrides the _checkTarget method within FabricJS to reject clicks that are more than a specified distance from the border (defined by the clickableMargin variable).
//sets the width of clickable area
var clickableMargin = 15;
var canvas = new fabric.Canvas("canvas");
canvas.add(new fabric.Rect({
width: 150,
height: 150,
left: 25,
top: 25,
fill: 'green',
strokeWidth: 0
}));
//overrides the _checkTarget method to add check if point is close to the border
fabric.Canvas.prototype._checkTarget = function(pointer, obj, globalPointer) {
if (obj &&
obj.visible &&
obj.evented &&
this.containsPoint(null, obj, pointer)){
if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) {
var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y);
if (!isTransparent) {
return true;
}
}
else {
var isInsideBorder = this.isInsideBorder(obj);
if(!isInsideBorder) {
return true;
}
}
}
}
fabric.Canvas.prototype.isInsideBorder = function(target) {
var pointerCoords = target.getLocalPointer();
if(pointerCoords.x > clickableMargin &&
pointerCoords.x < target.getScaledWidth() - clickableMargin &&
pointerCoords.y > clickableMargin &&
pointerCoords.y < target.getScaledHeight() - clickableMargin) {
return true;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="canvas" height="300" width="400"></canvas>
Fabric.js uses Object.containsPoint() to determine whether a mouse event should target the object. This method, in turn, calculates the object's edges via Object._getImageLines() and checks how many times the projection of a mouse pointer crossed those lines.
The solution below calculates additional inner edges based on the coordinates of each corner, therefore object scale and rotation are taken care of automatically.
const canvas = new fabric.Canvas('c', {
enableRetinaScaling: true
})
const rect = new fabric.Rect({
left: 0,
top: 0,
width: 100,
height: 100,
dragBorderWidth: 15, // this is the custom attribute we've introduced
})
function innerCornerPoint(start, end, offset) {
// vector length
const l = start.distanceFrom(end)
// unit vector
const uv = new fabric.Point((end.x - start.x) / l, (end.y - start.y) / l)
// point on the vector at a given offset but no further than side length
const p = start.add(uv.multiply(Math.min(offset, l)))
// rotate point
return fabric.util.rotatePoint(p, start, fabric.util.degreesToRadians(45))
}
rect._getInnerBorderLines = function(c) {
// the actual offset from outer corner is the length of a hypotenuse of a right triangle with border widths as 2 sides
const offset = Math.sqrt(2 * (this.dragBorderWidth ** 2))
// find 4 inner corners as offsets rotated 45 degrees CW
const newCoords = {
tl: innerCornerPoint(c.tl, c.tr, offset),
tr: innerCornerPoint(c.tr, c.br, offset),
br: innerCornerPoint(c.br, c.bl, offset),
bl: innerCornerPoint(c.bl, c.tl, offset),
}
return this._getImageLines(newCoords)
}
rect.containsPoint = function(point, lines, absolute, calculate) {
const coords = calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords
lines = lines || this._getImageLines(coords)
const innerRectPoints = this._findCrossPoints(point, lines);
const innerBorderPoints = this._findCrossPoints(point, this._getInnerBorderLines(coords))
// calculate intersections
return innerRectPoints === 1 && innerBorderPoints !== 1
}
canvas.add(rect)
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="c" width="400" height="300"></canvas>
here is my approach, when rect is clicked I am calculating where it is clicked and
if it is not clicked on border I have to set canvas.discardActiveObject , see comments on code
var canvas = new fabric.Canvas('c', {
selection: false
});
var rect = new fabric.Rect({
left: 50,
top: 50,
width: 100,
height: 100,
strokeWidth: 10,
stroke: 'red',
selectable: false,
evented: true,
hasBorders: true,
lockMovementY: true,
lockMovementX: true
})
canvas.on("mouse:move", function(e) {
if (!e.target || e.target.type != 'rect') return;
// when selected event is fired get the click position.
var pointer = canvas.getPointer(e.e);
// calculate the click distance from object to be exact
var distanceX = pointer.x - rect.left;
var distanceY = pointer.y - rect.top;
// check if click distanceX/Y are less than 10 (strokeWidth) or greater than 90 ( rect width = 100)
if ((distanceX <= rect.strokeWidth || distanceX >= (rect.width - rect.strokeWidth)) || (distanceY <= rect.strokeWidth || distanceY >= (rect.height - rect.strokeWidth))) {
rect.set({
hoverCursor: 'move',
selectable: true,
lockMovementY: false,
lockMovementX: false
});
document.getElementById('result').innerHTML = 'on border';
} else {
canvas.discardActiveObject();
document.getElementById('result').innerHTML = 'not on border';
rect.set({
hoverCursor: 'default',
selectable: false,
lockMovementY: true,
lockMovementX: true
});
}
});
canvas.add(rect);
canvas.renderAll();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<div id="result" style="width: 100%; "></div>
<canvas id="c" width="600" height="200"></canvas>
<pre>
</pre>
ps: you can also set the rect property to selectable: false and call canvas.setActiveObject(this); to make it selection inside if statement.

Change control fill color while scaling object in Fabricjs

For a few days now, I have been trying to change the fill color of the control that is scaling an object.
Here is a gif of what I'm talking about:
I would like some guidance on how to achieve this. I have been digging through Fabricjs documentation for days trying to get an idea on how to approach this problem.
https://github.com/fabricjs/fabric.js/wiki/Working-with-events
My theory was to bind to mouse:down and mouse:up events. When mouse:down event fires, obtain the control context and change its fill color and when the mouse:up fires, restore the fill color.
Unfortunately, I can't find any fabricjs method that would allow me to obtain the control context.
http://fabricjs.com/docs/fabric.Canvas.html
http://fabricjs.com/docs/fabric.Object.html
canvas.on('mouse:down',(){
// Obtain control context and change fill
});
canvas.on('mouse:up',(){
// Obtain control context and restore fill
});
I'm using Fabricjs version 3.2.0
I overwrote the _drawControl from fabric.Object. As you can see I verified this.__corner control variable, If are equal I changed the fill and stroke color to red. Very important I restored the context after I drawn the control
var canvas = new fabric.Canvas('fabriccanvas');
canvas.add(new fabric.Rect({
top: 50,
left: 50 ,
width: 100,
height: 100,
fill: '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6),
//fix attributes applied for all rects
cornerStyle:"circle",
originX: 'left',
originY: 'top',
transparentCorners:false
}));
fabric.Object.prototype._drawControl = function(control, ctx, methodName, left, top, styleOverride) {
styleOverride = styleOverride || {};
if (!this.isControlVisible(control)) {
return;
}
var size = this.cornerSize, stroke = !this.transparentCorners && this.cornerStrokeColor;
switch (styleOverride.cornerStyle || this.cornerStyle) {
case 'circle':
if(control == this.__corner){
ctx.save();
ctx.strokeStyle = ctx.fillStyle='red';
}
ctx.beginPath();
ctx.arc(left + size / 2, top + size / 2, size / 2, 0, 2 * Math.PI, false);
ctx[methodName]();
if (stroke) {
ctx.stroke();
}
if(control == this.__corner){
ctx.restore();
}
break;
default:
this.transparentCorners || ctx.clearRect(left, top, size, size);
ctx[methodName + 'Rect'](left, top, size, size);
if (stroke) {
ctx.strokeRect(left, top, size, size);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.2.0/fabric.js"></script>
<br/>
<canvas id="fabriccanvas" width="600" height="200" style="border:1px solid #ccc"></canvas>

How to keep scaling always at 1 on FabricJS [Edited]

I need in my application made in fabricJS, that when I modify an object with the scaling handlers, the scaling stays in 1, and that the size of the object takes the value of the height multiplied by scaleX. I can apply the same for the width. But I can not apply this in the images. How can I fix this?
I leave an example fiddle for a square, and an image, so that you can see the result.
The result in the image is similar to a crop, and I can not avoid this, because apparently, when I modify the container of the image, the _element property that also contains scaleX and scaleY does not change, so the image is not resized inside the container.
Here is jsFiddle.
var canvas = new fabric.Canvas('canvas');
var rect1 = new fabric.Rect({
width: 100,
height: 100,
left: 200,
top: 200,
angle: 0,
fill: 'rgba(0,0,255,1)',
originX: 'center',
originY: 'center'
});
canvas.add(rect1);
fabric.Image.fromURL('http://fabricjs.com/assets/pug_small.jpg', function(myImg) {
//i create an extra var for to change some image properties
var img1 = myImg.set({
left: 0,
top: 0,
width: 150,
height: 150
});
canvas.add(img1);
});
canvas.on('mouse:up', function(e) {
/*if(e.target != null){
//alert(e.target.width*e.target.scaleX);
}*/
//console.log(e.target);
canvas.getActiveObjects().forEach(function(obj) {
//if(obj.get('type')!='image'){
obj.set('width', obj.width * obj.scaleX, obj);
obj.set('height', obj.height * obj.scaleY, obj);
//if(obj.get('type')!='image'){
obj.scaleX = 1;
obj.scaleY = 1;
//}else{
// obj.scaleX = 1;
// obj.scaleY = 1;
// obj._element.scaleX = 1;
// obj._element.scaleY = 1;
// console.log("Width Contenedor Imagen: "+obj.width+" Width Imagen: "+obj._element.width);
// console.log("Scale Contenedor Imagen: "+obj.scaleX+" Scale Imagen: "+obj._element.scaleX);
//}
obj.setCoords();
//}
});
});
EDIT: New Discovery
Speaking of text, rect, images and polylines, it seems that I am modifying an external container, because the inner content remains intact. How can I modify the size of that let's say, inner container?

How to get correct height and width of Fabric.js object after modifying

After drawing an object and modifying the object with the mouse, the coordinates(Object.width and Object.height) remain the same as the originally drawn object.
const button = document.querySelector('button');
function load() {
const canvas = new fabric.Canvas('c');
const rect = new fabric.Rect({
width: 100,
height: 100,
left: 10,
top: 10,
fill: 'yellow',
});
function objectAddedListener(ev) {
let target = ev.target;
console.log('left', target.left, 'top', target.top, 'width', target.width, 'height', target.height);
}
function objectMovedListener(ev) {
let target = ev.target;
console.log('left', target.left, 'top', target.top, 'width', target.width, 'height', target.height);
}
canvas.on('object:added', objectAddedListener);
canvas.on('object:modified', objectMovedListener);
canvas.add(rect);
}
load();
button.addEventListener('click', load);
See codepen
width = target.width * target.scaleX,
height = target.height * target.scaleY
As we are scalling, so you need to multiply the scaleX and scaleY value to width and height respectively. Here is updated codepen
Currently You can use:
this.yourObject.getScaledWidth();
this.yourObject.getScaledHeight();
Which returns the same result.

Control z-index in Fabric.js

In fabricjs, I want to create a scene in which the object under the mouse rises to the top of the scene in z-index, then once the mouse leaves that object, it goes back to the z-index where it came from. One cannot set object.zindex (which would be nice). Instead, I'm using a placeholder object which is put into the object list at the old position, and then the old object is put back in the position where it was in the list using canvas.insertAt. However this is not working.
See http://jsfiddle.net/rFSEV/ for the status of this.
var canvasS = new fabric.Canvas('canvasS', { renderOnAddition: false, hoverCursor: 'pointer', selection: false });
var bars = {}; //storage for bars (bar number indexed by group object)
var selectedBar = null; //selected bar (group object)
var placeholder = new fabric.Text("XXXXX", { fontSize: 12 });
//pass null or a bar
function selectBar(bar) {
if (selectedBar) {
//remove the old topmost bar and put it back in the right zindex
//PROBLEM: It doesn't go back; it stays at the same zindex
selectedBar.remove();
canvasS.insertAt(selectedBar, selectedBar.XZIndex, true);
selectedBar = null;
}
if (bar) {
//put a placeholder object ("XXX" for now) in the position
//where the bar was, and put the bar in the top position
//so it shows topmost
selectedBar = bar;
canvasS.insertAt(placeholder, selectedBar.XZIndex, true);
canvasS.add(bar);
canvasS.renderAll();
}
}
canvasS.on({
'mouse:move': function(e) {
//hook up dynamic zorder
if (!e.target) return;
if (bars[e.target])
selectBar(e.target);
else
selectBar(null);
},
});
var objcount = canvasS.getObjects().length;
//create bars
for (var i = 0; i < 20; ++i) {
var rect = new fabric.Rect({
left: 0,
top: 0,
rx: 3,
ry: 3,
stroke: 'red',
width: 200,
height: 25
});
rect.setGradientFill({
x1: 0,
y1: 0,
x2: 0,
y2: rect.height,
colorStops: {
0: '#080',
1: '#fff'
}
});
var text = new fabric.Text("Bar number " + (i+1), {
fontSize: 12
});
var group = new fabric.Group([ rect, text ], {
left: i + 101,
top: i * 4 + 26
});
group.hasControls = group.hasBorders = false;
//our properties (not part of fabric)
group.XBar = rect;
group.XZIndex = objcount++;
canvasS.add(group);
bars[group] = i;
}
canvasS.renderAll();
Since fabric.js version 1.1.4 a new method for zIndex manipulation is available:
canvas.moveTo(object, index);
object.moveTo(index);
I think this is helpful for your use case. I've updated your jsfiddle - i hope this is what you want:
jsfiddle
Also make sure you change z-index AFTER adding object to canvas.
So code will looks like:
canvas.add(object);
canvas.moveTo(object, index);
Otherwise fabricjs don`t care about z-indexes you setup.
After I added a line object, I was make the line appear under the object using:
canvas.add(line);
canvas.sendToBack(line);
Other options are
canvas.sendBackwards
canvas.sendToBack
canvas.bringForward
canvas.bringToFront
see: https://github.com/fabricjs/fabric.js/issues/135
You can modify your _chooseObjectsToRender method to have the following change at the end of it, and you'll be able to achieve css-style zIndexing.
objsToRender = objsToRender.sort(function(a, b) {
var sortValue = 0, az = a.zIndex || 0, bz = b.zIndex || 0;
if (az < bz) {
sortValue = -1;
}
else if (az > bz) {
sortValue = 1;
}
return sortValue;
});
https://github.com/fabricjs/fabric.js/pull/5088/files
You can use these two functions to get z-index of a fabric object and modify an object's z-index, since there is not specific method to modify z-index by object index :
fabric.Object.prototype.getZIndex = function() {
return this.canvas.getObjects().indexOf(this);
}
fabric.Canvas.prototype.moveToLayer = function(object,position) {
while(object.getZIndex() > position) {
this.sendBackwards(object);
}
}

Resources