My fiddle: http://jsfiddle.net/yeftc60v/1/
So I'm trying to get the obj.left to be based on the left side(wall) of the group instead of the center. So right now, when I try to get the left of an object, if it's on the left side, it will return a negative number.
I've tried both setOriginY and setOriginX but they don't have any effect.
I'm working around this by doing some maths based on the group's width.
Right now if you hit the "clone" button, the left values read negative values (-100.5 and -0.5). Instead they should be 0 and 99.5(I think), respectively.
Unfortunately, any active group's origin x and y will always be at center. This won't take originX and originY property into consideration, hence setting setOriginX and setOriginY won't have any effect.
To get around this, you need to manually calculate the object's top and left position respective to the selected group, like so ...
let objLeft = obj.left + (active.width / 2);
let objTop = obj.top + (active.height / 2);
ᴅᴇᴍᴏ
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect({
left: 50,
top: 50,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 1,
opacity: .8
});
var rect1 = new fabric.Rect({
left: 150,
top: 150,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 1,
opacity: .8
});
canvas.add(rect);
canvas.add(rect1);
function clone() {
let active = canvas.getActiveGroup();
if (active) {
active.forEachObject(obj => {
let clone = obj.clone();
// get object's left & top relative to the group
let objLeft = obj.left + (active.width / 2);
let objTop = obj.top + (active.height / 2);
clone.setLeft(objLeft + active.left);
clone.setTop(objTop + active.top);
canvas.add(clone);
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.11/fabric.min.js"></script>
<canvas id="c" width="300" height="300" style="border: 1px solid black;"></canvas>
<button onclick="clone()">
Clone
</button>
Related
I am using the library FabricJS to overlay text on canvas. I need to add padding (ideally just left & right) to a text element that includes the property textBackgroundColor.
Here is what I've tried so far:
let textObject = new fabric.Text('Black & White',{
fontFamily: this.theme.font,
fontSize: this.theme.size,
textBaseline: 'bottom',
textBackgroundColor: '#000000',
left: 0,
top: 0,
width: 100,
height: 40,
padding: 20,
fill: 'white',
});
The padding doesn't work as I anticipated. I have attempted to use the backgroundColor property but that adds background to the whole group block and not just the text.
I could add a a non-breaking space to achieve the same effect, but this doesn't seem like a reliable solution and I was hoping Fabric JS allowed this out-of-the-box. Any ideas how to achieve this?
Required solution (version on the right, with additional padding is what I would like):
I give you 2 answers for two subtly different cases.
Case 1 - padding around the bounding box of single of multi-line text.
The code follows the CSS approach where margin is outside of the box, as depicted by the red line, and padding is inside, as shown by the gold background. On the left hand image, black text background is what you get from the built-in 'textBackgroundColor'. The yellow area shows the padding currently applied. The right hand image shows the additional benefit when you harmonise the padding colour, an also that you can reduce opacity on the background whilst keeping the text full-opaque.
BTW the built-in 'padding' attribute for text pads in relation to the controlling border, but the background color fill does not cover the white-space created. In other words, it operates like CSS margin rather than CSS padding.
Therefore it is necessary to ignore this padding attribute, and instead introduce a coloured rect to give the background color required, grouping this with the text element and positioning accordingly.
Example snippet below.
var canvas = window._canvas = new fabric.Canvas('c');
// function to do the drawing. Could easily be accomodated into a class (excluding the canvas reset!)
function reset(pos)
{
canvas.clear();
// Create the text node - note the position is (0, 0)
var text = new fabric.Text(pos.text, {
fontFamily: 'Arial',
left: 0,
top: 0,
fill: "#ffffff",
stroke: "",
textBackgroundColor: '#000000'
});
// create the outer 'margin' rect, note the position is negatively offset for padding & margin
// and the width is sized from the dimensions of the text node plus 2 x (padding + margin).
var rectMargin = new fabric.Rect({
left: -1 * (pos.padding.left + pos.margin.left),
top: -1 * (pos.padding.top + pos.margin.top),
width: text.width + ((pos.padding.left + pos.padding.right) + (pos.margin.left + pos.margin.right)),
height: text.height + ((pos.padding.top + pos.padding.bottom) + (pos.margin.top + pos.margin.bottom)),
strokeWidth: pos.border,
stroke: 'red',
fill: 'transparent'
})
// create the inner 'padding' rect, note the position is offset for padding only
// and the width is sized from the dimensions of the text node plus 2 x padding.
var rectPadding = new fabric.Rect({
width: text.width + (pos.padding.left + pos.padding.right),
height: text.height + (pos.padding.top + pos.padding.bottom),
left: -1 * pos.padding.left, top: -1 * pos.padding.top,
fill: 'gold'
})
// create group and add shapes to group, rect first so it is below text.
// note that as the group is oversized, we position it at pos - padding.
var group = new fabric.Group([ rectMargin, rectPadding, text ], {
left: pos.x - (pos.padding.left - pos.margin.left),
top: pos.y - (pos.padding.top - pos.margin.top),
angle: pos.angle,
});
canvas.add(group);
}
// function to grab values from user inputs
function go()
{
var m = $('#margin').val().split(',');
var p = $('#padding').val().split(',');
for (var i = 0 ; i < 4; i = i + 1)
{
p[i] = parseInt(p[i], 10); // ensure we have numbers and not strings !
m[i] = parseInt(m[i], 10);
}
// Object holding position and content info
var pos = {x: 50, y : 10, text: 'Text with padding\nand another line',
padding: {top:p[0], right:p[1], bottom: p[2], left: p[3]}, margin: {top:m[0], right:m[1], bottom: m[2], left: m[3]}, border: 1, angle: 10};
reset(pos);
}
// click handler for go button
$('#go').on('click', function(e){
go();
})
// call go once to show on load
go();
div
{
background-color: silver;
width: 600px;
height: 300px;
}
.ipt
{
margin-right: 20px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<p>
<span class='ipt'> Margin: <input id='margin' value = '12,10,12,10' /></span>
<span class='ipt'> Padding: <input id='padding' value = '0,5,0,5' /></span>
<span class='ipt'><button id='go' />Go</button></span>
<div>
<canvas id="c" width="600" height="300"></canvas>
</div>
Case 2: padding boxing the individual text lines and not the full bounding box.
In this case you can see the difference in how the padded background tracks each line of text instead of applying to the outer bounding box of the text. The solution is more complex, involving creating a dummy text node which then provides line splitting and sizing information. We then loop thru the line data, outputting individual text lines and padding rects into a group which means we can position and handle the text as a single object, as illustrated by the applied angle.
var textIn = 'Text goat\nMillenium jam\nplumb\nBlack & White'
var canvas = window._canvas = new fabric.Canvas('c');
// function to do the drawing. Could easily be accomodated into a class (excluding the canvas reset!)
function reset(pos)
{
canvas.clear();
// Create the text measuring node - not added to the canvas !
var textMeasure = new fabric.IText(pos.text, {
fontFamily: 'Arial',
left: 0,
top: 0,
fill: "#ffffff",
stroke: "",
textBackgroundColor: '#000000'
});
// loop round the lines in the text creating a margin/pad scenario for each line
var theText, text, textHeight, rectPadding, rectMargin, top = 0, shapes = [];
for (var i = 0; i < textMeasure._textLines.length; i = i + 1){
theText = textMeasure._textLines[i].join('');
textHeight = Math.floor(textMeasure.lineHeight * textMeasure.fontSize) //textMeasure.getHeightOfLine(i)
// Make the text node for line i
text = new fabric.IText(theText, {
fontFamily: 'Arial',
left: 0,
top: top,
fill: "#ffffff",
stroke: ""
});
// create the outer 'margin' rect, note the position is negatively offset for padding & margin
// and the width is sized from the dimensions of the text node plus 2 x (padding + margin).
rectMargin = new fabric.Rect({
left: -1 * (pos.padding.left + pos.margin.left),
top: top - (pos.padding.top + pos.margin.top),
width: text.width + ((pos.padding.left + pos.padding.right) + (pos.margin.left + pos.margin.right)),
height: textHeight + ((pos.padding.top + pos.padding.bottom) + (pos.margin.top + pos.margin.bottom)),
fill: 'transparent'
})
shapes.push(rectMargin);
// create the inner 'padding' rect, note the position is offset for padding only
// and the width is sized from the dimensions of the text node plus 2 x padding.
rectPadding = new fabric.Rect({
width: text.width + (pos.padding.left + pos.padding.right),
height: textHeight + (pos.padding.top + pos.padding.bottom),
left: -1 * pos.padding.left,
top: top - pos.padding.top,
fill: '#000000ff'
})
shapes.push(rectPadding);
shapes.push(text);
// move the insert point down by the height of the line
var gap = 0; // text.lineHeight - textHeight;
top = top - 1 + textHeight + pos.padding.top + pos.margin.top + pos.padding.bottom + pos.margin.bottom;
}
// At this point we have a list of shapes to output in the shapes[] array.
// Create group and add the shapes to group.
// note that group is positioned so that the topleft of the first text line is where
// it would fall if it were a standard text node.
var group = new fabric.Group(shapes, {
left: pos.x - (pos.padding.left - pos.margin.left),
top: pos.y - (pos.padding.top - pos.margin.top),
angle: pos.angle,
});
canvas.add(group);
}
// function to grab values from user inputs
function go()
{
var m = $('#margin').val().split(',');
var p = $('#padding').val().split(',');
for (var i = 0 ; i < 4; i = i + 1)
{
p[i] = parseInt(p[i], 10); // ensure we have numbers and not strings !
m[i] = parseInt(m[i], 10);
}
// Object holding position and content info
var pos = {x: 70, y : 10, text: textIn,
padding: {top:p[0], right:p[1], bottom: p[2], left: p[3]}, margin: {top:m[0], right:m[1], bottom: m[2], left: m[3]}, border: 1, angle: 10};
reset(pos);
}
// click handler for go button
$('#go').on('click', function(e){
go();
})
// call go once to show on load
go();
div
{
background-color: silver;
width: 600px;
height: 100px;
}
.ipt
{
margin-right: 20px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<p>
<span class='ipt'> Margin: <input id='margin' value = '0,0,0,0' /></span>
<span class='ipt'> Padding: <input id='padding' value = '5,15,5,15' /></span>
<span class='ipt'><button id='go' />Go</button></span>
<div>
<canvas id="c" width="600" height="300"></canvas>
</div>
If anyone is looking for a solution with Textbox,
Here's another solution you can try: https://github.com/fabricjs/fabric.js/issues/3731
var TextboxWithPadding = fabric.util.createClass(fabric.Textbox, {
_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
);
// if there is background color no other shadows
// should be casted
this._removeShadow(ctx);
}
});
Extend the base class and overwrite the _renderBackground function.
Very simple and works well for me!
I recently started to work with fabricjs, and I have a question about connectivity object. i do creating sample demo for connectivity from parent object to child object connect with arrow. I got sample demo http://kpomservices.com/HTML5CanvasCampaign/campaign.html. This code should work with fabric 1.4.2.A similar code is possible for the version 2.3.3 not work.
Error functions.js:963 Uncaught TypeError: c.setAngle is not a function
code
function makeArrow(centerpt, left, top, line) {
var c = new fabric.Triangle({
width: 10,
height: 10,
left: left,
top: top,
//selectable: false,
strokeWidth: 3,
fill: 'grey',
opacity: 1,
stroke: 'grey',
originX: 'center',
originY: 'center'
});
c.hasBorders = c.hasControls = false;
c.angle = 90;
c.line = line;
var dx = left - centerpt.x;
var dy = top - centerpt.y;
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
c.setAngle(angle + 90);
c.setCoords();
c.name = 'ep';
line.ep = c;
c.line = line;
return c;
}
Build doesn't have setter/getter (optional). You can make your build here with Named accessors. If you want to set angle you can use
obj.angle = text;
//or
obj.set({
angle:angle
});
//or
obj.set('angle', angle);
DEMO
var canvas = new fabric.Canvas('c');
var triangle = new fabric.Triangle({
left: 150,
top: 50,
width: 300,
height:200,
fill:'',
stroke:'red'
})
canvas.add(triangle);
triangle.set('angle',40);
//triangle.angle = 40;
//triangle.set({angle : 40})
canvas{
border:1px solid #000;
}
<script type="text/javascript" src="
https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="400" height="400"></canvas>
I'm using fabricjs (1.7.20) and would like to create a sort of "bleed area" where some space around the canvas isn't usable by the user; a sort of "wall" if you will, to stop objects from being moved to the sides of the canvas walls. How might I accomplish this?
var canvas = new fabric.Canvas("c");
canvas.setHeight(350);
canvas.setWidth(350);
canvas.add(new fabric.IText("Some text", {
top: 25,
}));
var circle = new fabric.Circle({
radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
width: 20, height: 30, fill: 'blue', left: 150, top: 150
});
canvas.add(circle, triangle);
canvas {
border: 1px solid #dddddd;
margin-top: 10px;
border-radius: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.20/fabric.min.js"></script>
<canvas id="c"></canvas>
This was suggested to me and it is the closest I've gotten. I'm looking to do this, but 10px, give or take, from the border of the canvas.
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);
}
});
You can add a 10 pixel bleed area by adding / subtracting the value 10 from the conditions that determine if an object is being moved out of the “bleed” area (I'd actually prefer to call it “padding”), and adding / subtracting from the calculation that repositions the object inside the padding boundaries.
Here's an updated example that works:
var padding = 10;
var canvas = new fabric.Canvas("c");
canvas.setHeight(350);
canvas.setWidth(350);
canvas.add(new fabric.IText("Some text", {
top: 25,
}));
var circle = new fabric.Circle({
radius: 20,
fill: 'green',
left: 100,
top: 100
});
var triangle = new fabric.Triangle({
width: 20,
height: 30,
fill: 'blue',
left: 150,
top: 150
});
canvas.add(circle, triangle);
canvas.on('object:moving', function(e) {
var obj = e.target;
// if object is too big ignore
if (obj.currentHeight > obj.canvas.height - padding * 2 ||
obj.currentWidth > obj.canvas.width - padding * 2) {
return;
}
obj.setCoords();
// top-left corner
if (obj.getBoundingRect().top < padding ||
obj.getBoundingRect().left < padding) {
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top + padding);
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left + padding);
}
// bot-right corner
if (obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height - padding ||
obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width - padding) {
obj.top = Math.min(
obj.top,
obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top - padding);
obj.left = Math.min(
obj.left,
obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left - padding);
}
});
canvas {
border: 1px solid #dddddd;
margin-top: 10px;
border-radius: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.20/fabric.min.js"></script>
<canvas id="c"></canvas>
In the first line, I've defined a variable “padding” that refers to the desired padding size. This way, if you want to change the padding later on, you don't have to change it in eight different places.
The padding variable used instead of 0 in the condition for the top left corner.
If the “top left” condition is true, the padding is added to the calculation for repositioning the object.
In the “bottom right” part, we are doing the opposite – we subtract the padding from the condition and subtract from the repositioning calculation.
You can also try the code in this fiddle: https://jsfiddle.net/pahund/hz7jLnme/
So I want to save the canvas based on scale 1 and I want to include all existing/visible objects.
So right now, my canvas is 600x400 and if I were to save it, it would only save what's inside that 600x400, and it respects zoom level (a higher zoom will see less things, a lower zoom will see more things but smaller).
I've gotten around this by running this code:
let zoom = this.canvas.getZoom();
this.canvas.setZoom(1);
let url = this.canvas.toDataURL({width: 1000, height: 700, multiplier: 1});
this.canvas.setZoom(zoom);
window.open(
url,
'_blank' // <- This is what makes it open in a new window.
);
What this does is save the image as 1000x700, so stuff that were past 600px but under 1000 still gets saved.
However, this is hard coded. So I was wondering if there was an existing function or a clean/simple way to detect where all the objects are in and returns the full size (width and height).
fiddle: http://jsfiddle.net/1pxceaLj/3/
Update 1:
var gr = new this.fabric.Group([], {
left: 0,
top: 0
});
this.canvas.forEachObject( (o) => {
gr.add(o);
});
this.canvas.add(gr);
this.canvas.renderAll();
console.log(gr.height, gr.width); // 0 0
Solution
Using group was the best idea. Best example: http://jsfiddle.net/softvar/mRA8Z/
function selectAllCanvasObjects(){
var objs = canvas.getObjects().map(function(o) {
return o.set('active', true);
});
var group = new fabric.Group(objs, {
originX: 'center',
originY: 'center'
});
canvas._activeObject = null;
canvas.setActiveGroup(group.setCoords()).renderAll();
console.log(canvas.getActiveGroup().height);
}
One possibility would be to create a kind of Container using a fabricjs group and adding all created objects to this container.
I updated your fiddle here: http://jsfiddle.net/1pxceaLj/4/
Thus, you could just use group.width and group.height, perhaps adding a little offset or minimum values, and there you are having dynamical value to pass into toDataUrl even when having a smaller canvas.
code:
var canvas = new fabric.Canvas('c');
var shadow = {
color: 'rgba(0,0,0,0.6)',
blur: 20,
offsetX: 10,
offsetY: 10,
opacity: 0.6,
fillShadow: true,
strokeShadow: true
}
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 10,
opacity: .8
});
var rect2 = new fabric.Rect({
left: 800,
top: 100,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 10,
opacity: .8
});
rect.setShadow(shadow);
//canvas.add(rect);
//canvas.add(rect2);
var gr = new fabric.Group([ rect, rect2 ], {
left: 0,
top: 0
});
canvas.add(gr);
function save()
{
alert(gr.width);
alert(gr.height);
let zoom = canvas.getZoom();
var minheight = 700;
canvas.setZoom(1);
let url = this.canvas.toDataURL({width: gr.width, height: gr.height > minheight ? gr.height : minheight, multiplier: 1});
canvas.setZoom(zoom);
window.open(
url,
'_blank'
);
}
I have designed a ruler using fabric.js and when the user mouses over the specific part of the ruler I want to print to the screen the X-coordinate of the ruler (i.e. not the screen coordinate). Right now the ruler canvas starts at position 37 and ends at 726 relative to the ruler, but the ruler goes from 1 to 4600 (it will always start at 1 but the length of the ruler can change). How to transform the mouse coordinates to accurately reflect it's position on the ruler? Here is the code:
var canvas = new fabric.Canvas('canvas');
line_length = input_len;
adjusted_length = (line_length / 666) * 100;
canvas.add(new fabric.Line([0, 0, adjusted_length, 0], {
left: 30,
top: 0,
stroke: '#d89300',
strokeWidth: 3
}));
$('#canvas_container').css('overflow-x', 'scroll');
$('#canvas_container').css('overflow-y', 'hidden');
drawRuler();
function drawRuler() {
$("#ruler[data-items]").val(line_length / 200);
$("#ruler[data-items]").each(function () {
var ruler = $(this).empty(),
len = Number($("#ruler[data-items]").val()) || 0,
item = $(document.createElement("li")),
i;
ruler.append(item.clone().text(1));
for (i = 1; i < len; i++) {
ruler.append(item.clone().text(i * 200));
}
ruler.append(item.clone().text(i * 200));
});
}
canvas.add(new fabric.Text('X-cord', {
fontStyle: 'italic',
fontFamily: 'Hoefler Text',
fontSize: 12,
left: 0,
top: 0,
hasControls: false,
selectable: false
}));
canvas.on('mouse:move', function (options) {
getMouse(options);
});
function getMouse(options) {
canvas.getObjects('text')[0].text =
"X-coords: " + options.e.clientX ; //+ " Y: " + options.e.clientY;
canvas.renderAll();
}
Use the getPointer merthod on canvas Instance.
In your case it should be canvas.getPointer(options.e)which returns an object with x and y properties which represent pointer coordinates relative to canvas.