I figured out the only way to make a shape larger is by scaling it, not by setting a width and height. So I scaled the shape like this:
toObject: function() {
return fabric.util.object.extend(this.callSuper("toObject"), {
objectSize: this.objectSize,
scaleX: this.scaleX,
scaleY: this.scaleY,
labelColor: this.labelColor,
hasRotatingPoint: this.hasRotatingPoint,
hasControls: this.hasControls,
evented: this.evented,
hoverCursor: this.hoverCursor,
_controlsVisibility: this._controlsVisibility
});
},
but the ctx.fillText also scales with it. That is not what I want. The text needs to stay the same. So i did the following:
_render: function(ctx) {
this.callSuper("_render", ctx);
ctx.font = "12px Helvetica";
ctx.fillStyle = this.labelColor;
ctx.scale(1 / this.scaleX, 1 / this.scaleY);
ctx.fillText(
this.objectSize,
-this.objectSize.split('x')[0] / 2 + 10,
-this.objectSize.split('x')[1] / 2 - 5
);
}
Now the issue is that at some shape sizes (for example 200x200) the object also scales with it. When I remove the ctx.scale(1 / this.scaleX, 1 / this.scaleY) the object has the right size, except then the text is also scaled up.
How can I scale the shape without scaling the text or visa versa.
Related
I'm new to fabricjs (and to Javascript development in general). I am "porting" a legacy Flex/Actionscript project and need to enable the user to create a complex clipping path for an image.
My approach in Actionscript was to use the Actionscript Graphics class using BlendMode.ERASE to "erase" from the yellow base rectangle (i.e. give the appearance of erasing) and then using that set of rects to create a bitmap to serve as an alpha channel for the final image (Step 3) created on the fly.
Can anyone suggest how I might accomplish a similar functionality in Fabric? It doesn't seem to support HTML5 Canvas Blend modes and while I see that it supports clipping paths for images, I'm not seeing how I can enable the user to interactively create a clipping path without doing lots of intersection checks to try to derive the points to create a new path on the fly.
Thanks!
Step 1: After the user has drawn a base rectangle, drag-option/alt-key enables them to draw a rectangle (the red line) which will be subtracted from the base rect.
Step 2: The base rect is shown with the subtraction.
Step 3: The base rect is used to clip or mask a section of the base image
Step 1
Step 2
Step 3
Will Tower,
There is no easy way to do it. Here are the steps:
Draw 'Yellow' rectangle
Draw 'Red' rectangle
Use clipping library like PolyBool for intersection and xor operations
Convert drawing result into the clipped path of combining rectangles
clip your image
I created some quick fiddle. You have to click on a each button to clip. It won't clip if you will not add 2 rectangles on the canvas. This is very simple example. In order to work properly you have to draw rectangles with mouse (make them dynamic). Also, this logic is not accounting for these variations (you have to work on them as well):
For these use cases Clipping Library will return to you 2 set of results, which means different logic should be implemented.
Actual code without jQuery, FabriJs, and PolyBool libraries:
var imgURL = 'http://fabricjs.com/lib/pug.jpg';
var clipYellowRect = null;
var clipRedRect = null;
var pug = null;
var canvas = new fabric.Canvas('c');
// insert image into canvas
var pugImg = new Image();
pugImg.onload = function (img) {
pug = new fabric.Image(pugImg, {
angle: 0,
width: 500,
height: 500,
left: 100,
top: 50,
scaleX: 0.5,
scaleY: 0.5,
clipName: 'pug',
});
canvas.add(pug);
};
pugImg.src = imgURL;
//draw yellow rectangle
$('#btnYellowRect').on('click', function(){
clipYellowRect = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 120,
top: 60,
width: 200,
height: 200,
fill: 'rgba(255,255,0,0.5)',
strokeWidth: 0,
selectable: false
});
canvas.add(clipYellowRect);
});
//draw red rectangle
$('#btnRedRect').on('click', function(){
clipRedRect = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 90,
top: 120,
width: 100,
height: 100,
strokeWidth: 3,
fill: 'transparent',
stroke: 'rgba(255,0,0,1)', /* use transparent for no fill */
strokeWidth: 0,
selectable: false
});
canvas.add(clipRedRect);
});
//clip
$('#btnClip').on('click', function(){
var yellowRectRegion = getRegion(clipYellowRect);
var redRectRegion = getRegion(clipRedRect);
//determine inersection
var intersectResult = PolyBool.intersect({
regions: [yellowRectRegion],
inverted: false
}, {
regions: [redRectRegion],
inverted: false
});
//generate clipping path
var xorResult = PolyBool.xor({
regions: [yellowRectRegion],
inverted: false
}, {
regions: intersectResult.regions,
inverted: false
});
clipImage(xorResult.regions[0]);
});
//prepare data for clipping library
function getRegion(rect){
return [[rect.left, rect.top],
[rect.left + rect.width, rect.top],
[rect.left + rect.width, rect.top + rect.height],
[rect.left, rect.top + rect.height]]
}
function clipImage(points){
//actual clipping
pug.clipTo = function (ctx) {
var scaleXTo1 = (1 / pug.scaleX);
var scaleYTo1 = (1 / pug.scaleY);
ctx.save();
var ctxLeft = -( pug.width / 2 );
var ctxTop = -( pug.height / 2 );
ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
console.log(points)
ctx.moveTo(points[0][0] - pug.oCoords.tl.x, points[0][1] - pug.oCoords.tl.y);
for (var i=1; i < points.length; i++){
ctx.lineTo(points[i][0] - pug.oCoords.tl.x, points[i][1] - pug.oCoords.tl.y);
}
ctx.closePath();
ctx.restore();
};
clipYellowRect.remove();
clipRedRect.remove();
canvas.renderAll();
}
Hopefully it will help you.
I need to draw customized rectangle - the one with 4 "ears" in each corner.
So I defined my own class based on fabric.Object like this:
var CustomRect = fabric.util.createClass(fabric.Object, {
...
}
Important note: when creating objects based on my class CustomRect I want to define width and height as a size of a rectangle without those "ears".
But I have a problem. I can not draw outside of an area defined by width x height.
Everything else gets clipped - it's drawn but not visible. So I'm not able to draw those "ears" while they go beyond the limits of an area width x height.
How to tell fabric to extend drawing area? How can I draw outside of width x height area?
Many thanks guys!
So I have found this solution. Not perfect but it works.
While Fabric adjusts the size of painting area for an object according to it's width and height parameters, I had to make my own width and height parameters.
Fabric's width and height are set to the size of painting area, while my width and height are set to the real size of a rectangle (rectangle without "ears").
Here we go, class definition first:
var CustomRect = fabric.util.createClass(fabric.Object, {
initialize: function(options) {
options || (options = { });
if (options.width || options.height){
alert('Do not use width/height, use my_width/my_height instead.');
}
// here we set our Object's width, height to create painting area for it
// It must be little larger than rectangle itself to paint "ears"
options.width = options.my_width + SIZE_EXTEND;
options.height = options.my_height + SIZE_EXTEND;
this.callSuper('initialize', options);
},
_render: function(ctx) {
var PIx2 = 6.28319; // 2 * PI
var w_half = (this.width - SIZE_EXTEND) / 2;
var h_half = (this.height - SIZE_EXTEND) / 2;
ctx.rect(this.left, this.top, this.width - SIZE_EXTEND, this.height - SIZE_EXTEND);
ctx.fill();
// "ears"
ctx.beginPath();
ctx.arc(-w_half, -h_half, 4, 0, PIx2, false);
ctx.fill();
ctx.beginPath();
ctx.arc(w_half, -h_half, 4, 0, PIx2, false);
ctx.fill();
ctx.beginPath();
ctx.arc(-w_half, h_half, 4, 0, PIx2, false);
ctx.fill();
ctx.beginPath();
ctx.arc(w_half, h_half, 4, 0, PIx2, false);
ctx.fill();
}
}
SIZE_EXTEND is a constant defined elsewhere (for my application constant is OK).
And now how I use it in my application. Adding a new rectangle onto my canvas:
var TheCanvas;
TheCanvas = new fabric.Canvas('mainCanvas');
TheCanvas.setWidth(window.innerWidth);
TheCanvas.setHeight(window.innerHeight);
TheCanvas.add(new CustomRect({
left: 500,
top: 200,
my_width: 100, // here I define size WITHOUT "ears"
my_height: 100
}));
The problem is to update the actual object.width, object.height and keep object.scaleX = 1 and object.scaleY = 1 when using the visual controls on the canvas.
The origin of the problem is when manipulating the object using controls, fabric.js changes the scale of the object, as well as scaling the stroke of paths, which I would like to be of the same width always.
So I would like to make it more like Adobe Illustrator-like, and not scale the width of the stroke along with the width of the rectalngular, for example.
I have found a solution to a question formulated in a different manner here:
Rect with stroke, the stroke line is mis-transformed when scaled
The idea is to calculate the new would-be width and height using the new scale factors, set the new dimensions and reset the scale factors to 1.
Here's the code example from that answer:
el.on({
'scaling': function(e) {
var obj = this,
w = obj.width * obj.scaleX,
h = obj.height * obj.scaleY,
s = obj.strokeWidth;
obj.set({
'height' : h,
'width' : w,
'scaleX' : 1,
'scaleY' : 1
});
}
});
From Fabric.js version 2.7.0 you can enable a strokeUniform property on the object this will always match the exact pixel size entered for stroke width.
var circle2 = new fabric.Circle({
radius: 65,
fill: '#4FC3F7',
left: 110,
opacity: 0.7,
stroke: 'blue',
strokeWidth: 3,
strokeUniform: true
});
http://fabricjs.com/stroke-uniform
i have scrambled my puzzzle, now i need to have it rotated when i scrmable? can anyone help? Thanks :)
fillPatternImage:imageObj,
x:-pieceWidth*i/2,
y:-pieceHeight*j/2,
stroke: "#000000",
strokeWidth: 4,
lineCap: "round",
draggable: true,
x: i+pieceWidth/4 + (Math.random()*4)*((stage.getWidth()+pieceWidth)/12),
y: j+pieceHeight/4 + (Math.random()*2)*((stage.getHeight()+pieceHeight)/12),
});
JsFiddle : http://jsfiddle.net/vFez6/25/
The general method to rotate a canvas drawing is:
save the context state: context.save()
translate to the desired center of the object: context.translate(centerX,centerY)
rotate by the desired radian angle: context.rotate(Math.PI/4)
draw the object offset by 1/2 width & height: 'context.fillRect(-width/2,-height/2,50,30)'
restore the context state to its unrotated state: 'context.restore()'
Example code and a Demo: http://jsfiddle.net/m1erickson/7aqwjddt/
rotateRectangle(150,150,75,50,45*Math.PI/180);
function rotateRectangle(centerX,centerY,width,height,radianAngle){
ctx.save();
ctx.translate(centerX,centerY);
ctx.rotate(radianAngle);
ctx.fillRect(-width/2,-height/2,width,height);
ctx.restore();
}
I'm trying to create an array of shapes that overlap. But I'm having difficulty preventing those shapes stacking on top of one-another.
I guess I want them to mesh together, if that makes sense?
Here's the code:
var overlap_canvas = document.getElementById("overlap");
var overlap_context = overlap_canvas.getContext("2d");
var x = 200;
var y = x;
var rectQTY = 4 // Number of rectangles
overlap_context.translate(x,y);
for (j=0;j<rectQTY;j++){ // Repeat for the number of rectangles
// Draw a rectangle
overlap_context.beginPath();
overlap_context.rect(-90, -100, 180, 80);
overlap_context.fillStyle = 'yellow';
overlap_context.fill();
overlap_context.lineWidth = 7;
overlap_context.strokeStyle = 'blue';
overlap_context.stroke();
// Degrees to rotate for next position
overlap_context.rotate((Math.PI/180)*360/rectQTY);
}
And here's my jsFiddle:
http://jsfiddle.net/Q8yjP/
And here's what I'm trying to achieve:
Any help or guidance would be greatly appreciated!
You cannot specify this behavior but you can implement an algorithmic-ish approach that uses composite modes.
As shown in this demo the result will be like this:
Define line width and the rectangles you want to draw (you can fill this array with the loop you already got to calculate the positions/angles - for simplicity I just use hard-coded ones here):
var lw = 4,
rects = [
[20, 15, 200, 75],
[150, 20, 75, 200],
[20, 150, 200, 75],
[15, 20, 75, 200]
], ...
I'll explain the line width below.
/// set line-width to half the size
ctx.lineWidth = lw * 0.5;
In the loop you add one criteria for the first draw which is also where you change composite mode. We also clear the canvas with the last rectangle:
/// loop through the array with rectangles
for(;r = rects[i]; i++) {
ctx.beginPath();
ctx.rect(r[0], r[1], r[2], r[3]);
ctx.fill();
ctx.stroke();
/// if first we do a clear with last rectangle and
/// then change composite mode and line width
if (i === 0) {
r = rects[rects.length - 1];
ctx.clearRect(r[0] - lw * 0.5, r[1] - lw * 0.5, r[2] + lw, r[3] + lw);
ctx.lineWidth = lw;
ctx.globalCompositeOperation = 'destination-over';
}
}
This will draw the rectangles and you have the flexibility to change the sizes without needing to recalculate clipping.
The line-width is set separately as stroke strokes the line from the middle. Therefor, since we later use destination-over mode it means half of the line won't be visible as we first fill which becomes part of destination so that the stroke will only be able to fill outside the stroked area (you could reverse the order of stroke and fill but will always run into an adjustment for the first rectangle).
We also need it to calculate the clipping which must include (half) the line on the outside.
This is also why we initially set it to half as the whole line will be drawn the first time - otherwise the first rectangle will have double as thick borders.
The only way to do it to cut your rectangles and compute which sub rectangle goes over which one. But I think you will have to draw your borders and inner rectangles separately because separating rectangles will add additional borders.
Hope it helped
Sadly, the feature you want of setting z-indexes on part of an element using canvas is not available currently. If you just need it for the four rectangle object you could do something like this which hides part of the rectangle to fake the effect you want, however this is hard coded to only 4 rectangles.
var overlap_canvas = document.getElementById("overlap");
var overlap_context = overlap_canvas.getContext("2d");
var x = 200;
var y = x;
var rectQTY = 4 // Number of rectangles
overlap_context.translate(x, y);
for (j = 0; j < rectQTY; j++) { // Repeat for the number of rectangles
// Draw a rectangle
overlap_context.beginPath();
overlap_context.rect(-90, -100, 180, 80);
overlap_context.fillStyle = 'yellow';
overlap_context.fill();
overlap_context.lineWidth = 7;
overlap_context.strokeStyle = 'blue';
overlap_context.stroke();
if (j === 3) {
overlap_context.beginPath();
overlap_context.rect(24, -86, 72, 80);
overlap_context.fillStyle = 'yellow';
overlap_context.fill();
overlap_context.closePath();
overlap_context.beginPath();
overlap_context.moveTo(20, -89.5);
overlap_context.lineTo(100, -89.5);
overlap_context.stroke();
overlap_context.closePath();
overlap_context.beginPath();
overlap_context.moveTo(20.5, -93.1);
overlap_context.lineTo(20.5, 23);
overlap_context.stroke();
overlap_context.closePath();
}
// Degrees to rotate for next position
overlap_context.rotate((Math.PI / 180) * 360 / rectQTY);
}
Demo here
If you have to make it dynamic, you could cut the shapes like Dark Duck suggested or you could try to create a function that detects when an object is overlapped and redraw it one time per rectangle (hard to do and not sure if it'd work). Perhaps you could come up with some equation for positioning the elements in relation to how I have them hard coded now to always work depending on the rotation angle, this would be your best bet IMO, but I don't know how to make that happen exactly
Overall you can't really do what you're looking for at this point in time
Using pure JavaScript ...
<!DOCTYPE html>
<html>
<head></head>
<body>
<canvas id="mycanvas" width="400px" height="400px"></canvas>
<script>
window.onload = function(){
var canvas = document.getElementById('mycanvas');
var ctx = canvas.getContext('2d');
//cheat - use a hidden canvas
var hidden = document.createElement('canvas');
hidden.width = 400;
hidden.height = 400;
var hiddenCtx = hidden.getContext('2d');
hiddenCtx.strokeStyle = 'blue';
hiddenCtx.fillStyle = 'yellow';
hiddenCtx.lineWidth = 5;
//translate origin to centre of hidden canvas, and draw 3/4 of the image
hiddenCtx.translate(200,200);
for(var i=0; i<3; i++){
hiddenCtx.fillRect(-170, -150, 300, 120);
hiddenCtx.strokeRect(-170, -150, 300, 120);
hiddenCtx.rotate(90*(Math.PI/180));
}
//reset the hidden canvas to original status
hiddenCtx.rotate(90*(Math.PI/180));
hiddenCtx.translate(-200,-200);
//translate to middle of visible canvas
ctx.translate(200, 200);
//repeat trick, this time copying from hidden to visible canvas
ctx.drawImage(hidden, 200, 0, 200, 400, 0, -200, 200, 400);
ctx.rotate(180*(Math.PI/180));
ctx.drawImage(hidden, 200, 0, 200, 400, 0, -200, 200, 400);
};
</script>
</body>
</html>
Demo on jsFiddle