Transparent object with shadow - javascript

Is there anyway to add shadow to transparent objects in FabricJS? I've used set fill transparent and after that setshadow. But normally setshadow can't be seen because object is transparent.

You can accomplish this using a clip path when drawing the object with the shadow. To do this with FabricJS you can subclass the object type you're generating the shadow from, and override the _render function to set a clip path that includes the area where the shadow falls but excludes the object itself. Ideally this reuses the code that actually draws the object.
fabric.BoxShadow = fabric.util.createClass(fabric.Rect, {
shadowColor: undefined,
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 0,
initialize(options) {
this.callSuper('initialize', options);
// Note: the way I have implemented this, the shadow settings cannot be changed after the object has been created.
this._shadow = new fabric.Shadow({
color: this.shadowColor,
blur: this.shadowBlur,
offsetX: this.shadowOffsetX,
offsetY: this.shadowOffsetY
});
},
_render: function(ctx) {
ctx.save();
// set clip path
let [offsetX, offsetY, blur] = [this.shadowOffsetX,
this.shadowOffsetY,
this.shadowBlur
];
let [top, left] = [this.width / -2, this.height / -2];
let region = new Path2D();
// The outer rectangle for our clipping path completely encompases the object and its shadow
let bounds = {
t: Math.min(top, top + offsetY - blur),
l: Math.min(left, left + offsetX - blur),
b: Math.max(top + this.height, top + this.height + offsetY + blur),
r: Math.max(left + this.width, left + this.width + offsetX + blur),
};
region.rect(bounds.l, bounds.t, bounds.r - bounds.l, bounds.b - bounds.t);
// now we subtract the actual object from our clipping path
// Note: we have to add beginPath function because the base class render code is going to treat this likc a CanvasRenderingContext2D instead of a Path2D
region.beginPath = function() { };
this.callSuper('_render', region);
ctx.clip(region, "evenodd");
// Fabric draws shadows, oddly enough, around the entire area rendered within this function. I haven't figured out the correct function to override to get our clip path to work with the normal fabric rendering pipeline
this.shadow = this._shadow;
// leverage the FabricJS shadow sizing logic
this._setShadow(ctx);
this.callSuper('_render', ctx);
this.shadow = undefined;
ctx.restore();
},
_renderPaintInOrder: function(ctx) {
if (ctx instanceof CanvasRenderingContext2D) {
this.callSuper('_renderPaintInOrder', ctx);
}
}
});
(function() {
let canvas = new fabric.Canvas('c');
fabric.Object.prototype.transparentCorners = false;
canvas.add(new fabric.Rect({
top: 40,
left: 40,
width: 100,
height: 100,
fill: 'lightblue'
}));
canvas.add(
new fabric.BoxShadow({
rx: 10,
top: 10,
left: 10,
width: 100,
height: 100,
shadowColor: 'black',
shadowBlur: 4,
shadowOffsetX: 3,
shadowOffsetY: 3
})
);
canvas.setWidth(document.body.clientWidth);
canvas.setHeight(document.body.clientHeight);
})();
html {
height: 100%;
}
body {
height: 100%;
}
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.7.0/fabric.js"></script>
</head>
<body>
<canvas id="c"></canvas>
</body>
</html>

I don't think the FabricJS API has a shadow-without-shape option.
But you can easily create the shadow-only using a native html5 canvas and then use that native canvas as an image source for a Fabric.Image object.
With native html5 canvas you can create a shadow without it's source shape like this:
Draw the shadowed shape,
Use compositing to "erase" the shape -- leaving just it's shadow
Example code drawing shadow-only on a native html5 canvas:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var shadowBlur=8;
var x=shadowBlur;
var y=shadowBlur;
var width=100;
var height=65;
canvas.width=width+shadowBlur*2;
canvas.height=height+shadowBlur*2;
// draw the shadowed shape
ctx.shadowColor='black';
ctx.shadowBlur=8;
ctx.fillRect(x-ctx.shadowOffsetX,y,width,height);
// always clean up! -- undo shadowing
ctx.shadowColor='rgba(0,0,0,0';
// use compositing to remove the shape
// (leaving just the shadow);
ctx.globalCompositeOperation='destination-out';
ctx.fillRect(x,y,width,height);
// always clean up! -- set compositing to default
ctx.globalCompositeOperation='source-over';
body{ background-color:white; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=512 height=512></canvas>
Example creating a Fabric.Image using native html5 canvas as an image source:
// where "canvas" is a reference to an html5 canvas element
var myFabricImage=new fabric.Image(canvas, { left:0, top:0 });

Related

Free drawing inside a object using fabric js

I loaded multiple objects from svg by following code:
var canvas = new fabric.Canvas('drawing');
fabric.loadSVGFromURL('images/circle.svg', function(objects) {
canvas.add.apply(canvas, objects);
canvas.forEachObject(function(o){ o.hasBorders = o.hasControls = false;
});
canvas.renderAll();
});
Now I want to free draw only inside one object(like the image below).How can I achieve that using fabric.js?
You can mask the canvas (using canvas.clipTo) and make it match your SVG form. If it is a circle as in your example image it would be simple.
See the following example:
// define a drawing canvas
const canvas = new fabric.Canvas(
'drawing',
{ isDrawingMode: true }
);
canvas.freeDrawingBrush.color = 'red'
// create a circle (here you could load your svg circle instead)
const circle = new fabric.Circle({
top: 25,
left: 25,
radius: 50,
fill: 'transparent',
stroke: 'black'
});
// create the clipping mask using the circle coordinates
canvas.clipTo = function (ctx) {
ctx.arc(
circle.top + circle.radius,
circle.left + circle.radius,
circle.radius + 1, // +1px for the circle stroke
circle.radius + 1,
Math.PI*2,
true
)
}
// add the circle to the canvas
canvas.add(circle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.5/fabric.min.js"></script>
<canvas id="drawing" width="600" height="600"></canvas>

Creating complex clipping path for image?

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.

Customized shape in Fabric - how to draw beyond it's size?

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
}));

Use a group as a mask in fabric.js

I want to create a group that is 300px wide and 200px high, and then load a few things inside that group. When I load images in that are larger than the group dimensions, it bleeds outside the group. I'd love to "crop" the image (similar to a CSS overflow:hidden property).
Is this possible?
To accomplish your task you should use the clipTo function on your image, the clipTo function on a group already has an open bug, btw you can work around there, by transpose the dimension and the position of your group to clipTo function:
clipTo :Function ยง Function that determines clipping of an object
(context is passed as a first argument) Note that context origin is at
the object's center point (not left/top corner)
Take a look to official demo, then after the clip operation on your image you can add it to a group(run below script to see an example).
var canvas = window.__canvas = new fabric.Canvas('c');
var path = 'http://fabricjs.com/lib/pug.jpg';
var _img = new Image();
_img.onload = function(img) {
var dog = new fabric.Image(_img, {
left: 100,
top: 100,
width: 300,
height: 300,
selectable: false,
clipName: 'dog',
clipTo: function(ctx) {
ctx.rect(0, 0, 50, 50);
}
});
var group = new fabric.Group([dog], {
left: 100,
top: 100,
width: 100,
height: 100,
borderColor: 'black',
});
canvas.add(group);
};
_img.src = path;
canvas.renderAll();
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.13/fabric.min.js"></script>
<canvas id="c" height="300" width="300" style="border:1px dashed #333;"></canvas>

Draw an outline outside the path surface

I have the following code to draw shapes (mainly used for rectangles) but the HTML5 drawing functions seem to draw borders with their thickness centered on the lines specified. I would like to have a border outside the surface of the shape and I'm at a loss.
Path.prototype.trace = function(elem, closePath) {
sd.context.beginPath();
sd.context.moveTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
sd.context.lineCap = "square";
for(var i=1; i<this.points.length; ++i) {
sd.context.lineTo(this.getStretchedX(i, elem.width), this.getStretchedY(i, elem.height));
}
if(closePath) {
sd.context.lineTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
}
}
getStrechedX and getStretchedY return the coordinates of the nth vertex once the shape is applied to a set element width, height and offset position.
Thanks to Ken Fyrstenberg's answer I've got it working for a rectangle, but this solution can sadly not apply to other shapes.
http://jsfiddle.net/0zq9mrch/
Here I drew two "wide" borders, one subtracting half the lineWidth to every position, another one adding. It doesn't work (as expected) because it's only going to put the thick lines above and to the left in one case, under and to the right in another - not "outside" the shape. You can also see a white area around the slope.
I tried working out how I could get the vertices to manually draw the path for the thick border (using fill() instead of stroke()).
But it turns out I still end up with the same problem: how to programatically determine if an edge is inside or outside. This would require some trigonometry and a heavy algorithm. For the purpose of my current work, this is too much trouble. I wanted to use this to draw a map of a building. The room walls need to be drawn outside the given dimensions, but I'll stick to standalone sloped walls for now.
Solution
You can solve this by drawing two lines:
First line with line thickness as intended
Second line contracted with 50% of the outer line width
To contract, add 50% to x and y, subtract line-width (or 2x 50%) from width and height.
Example
var ctx = document.querySelector("canvas").getContext("2d");
var lineWidth = 20;
var lw50 = lineWidth * 0.5;
// outer line
ctx.lineWidth = lineWidth; // intended line width
ctx.strokeStyle = "#975"; // color for main line
ctx.strokeRect(40, 40, 100, 100); // full line
// inner line
ctx.lineWidth = 2; // inner line width
ctx.strokeStyle = "#000"; // color for inner line
ctx.strokeRect(40 + lw50, 40 + lw50, 100 - lineWidth, 100 - lineWidth);
<canvas></canvas>
Complex shapes
For more complex shapes you will have to calculate the path manually. This is a little bit more complex and perhaps too broad for SO. You have to consider things like tangents, angle at bends, intersections and so forth.
One way to "cheat" is to:
draw the main line at full thickness to canvas
then use reuse the path as a clipping mask
change composite mode to destination-atop
draw the shape offset in various direction
restore clipping
change color and reuse path again for the main line.
The offset value below will determine the thickness of the inner line while the directions will determine resolution.
var ctx = document.querySelector("canvas").getContext("2d");
var lineWidth = 20;
var offset = 0.5; // line "thickness"
var directions = 8; // increase to increase details
var angleStep = 2 * Math.PI / 8;
// shape
ctx.lineWidth = lineWidth; // intended line width
ctx.strokeStyle = "#000"; // color for inner line
ctx.moveTo(50, 100); // some random shape
ctx.lineTo(100, 20);
ctx.lineTo(200, 100);
ctx.lineTo(300, 100);
ctx.lineTo(200, 200);
ctx.lineTo(50, 100);
ctx.closePath();
ctx.stroke();
ctx.save()
ctx.clip(); // set as clipping mask
ctx.globalCompositeOperation = "destination-atop"; // draws "behind" existing drawings
for(var a = 0; a < Math.PI * 2; a += angleStep) {
ctx.setTransform(1,0,0,1, offset * Math.cos(a), offset * Math.sin(a));
ctx.drawImage(ctx.canvas, 0, 0);
}
ctx.restore(); // removes clipping, comp. mode, transforms
// set new color and redraw same path as previous
ctx.strokeStyle = "#975"; // color for inner line
ctx.stroke();
<canvas height=250></canvas>
I'm late to the party, but here's an alternate way to "outside stroke" a complex path.
It uses a PathObject to simplify the process of creating the outside stroke.
The PathObject saves all the commands and arguments used to define your complex path.
This PathObject can also replay the commands--and can thereby redefine/redraw the saved path.
The PathObject class is re-usable. You can use it to save any path (simple or complex) that you need to redraw.
Html5 Canvas will soon have its own Path2D object built into the context, but my example below has a cross-browser polyfill that can be used until the Path2D object is implemented.
An illustration of a cloud with a silver lining applied using an outside stroke.
"Here's how it's done..."
Create a PathObject that can save all the commands and arguments used to define your complex path. This PathObject can also replay the commands--and can thereby redefine the saved path. Html5 Canvas will soon have its own Path2D object built into the context, but my example below is a cross-browser polyfill that can be used until the Path2D object is implemented.
Save a complex path using the PathObject.
Play the path commands on the main canvas and fill/stroke as desired.
Play the path commands on a temporary in-memory canvas.
On the temporary canvas:
Set a context.lineWidth of twice your desired outside stroke width and do the stroke.
Set globalCompositeOperation='destination-out' and fill. This will cause the inside of the complex path to be cleared and made transparent.
Draw the temporary canvas onto the main canvas. This causes your existing complex path on the main canvas to get the "outside stroke" from the in-memory canvas.
Here's example code and a Demo:
function log(){console.log.apply(console,arguments);}
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var canvas1=document.getElementById("canvas1");
var ctx1=canvas1.getContext("2d");
// A "class" that remembers (and can replay) all the
// commands & arguments used to define a context path
var PathObject=( function(){
// Path-related context methods that don't return a value
var methods = ['arc','beginPath','bezierCurveTo','clip','closePath',
'lineTo','moveTo','quadraticCurveTo','rect','restore','rotate',
'save','scale','setTransform','transform','translate','arcTo'];
var commands=[];
var args=[];
function PathObject(){
// add methods plus logging
for (var i=0;i<methods.length;i++){
var m = methods[i];
this[m] = (function(m){
return function () {
if(m=='beginPath'){
commands.length=0;
args.length=0;
}
commands.push(m);
args.push(arguments);
return(this);
};}(m));
}
};
// define/redefine the path by issuing all the saved
// path commands to the specified context
PathObject.prototype.definePath=function(context){
for(var i=0;i<commands.length;i++){
context[commands[i]].apply(context, args[i]);
}
}
//
PathObject.prototype.show=function(){
for(var i=0;i<commands.length;i++){
log(commands[i],args[i]);
}
}
//
return(PathObject);
})();
var x=75;
var y=100;
var scale=0.50;
// define a cloud path
var path=new PathObject()
.beginPath()
.save()
.translate(x,y)
.scale(scale,scale)
.moveTo(0, 0)
.bezierCurveTo(-40, 20, -40, 70, 60, 70)
.bezierCurveTo(80, 100, 150, 100, 170, 70)
.bezierCurveTo(250, 70, 250, 40, 220, 20)
.bezierCurveTo(260, -40, 200, -50, 170, -30)
.bezierCurveTo(150, -75, 80, -60, 80, -30)
.bezierCurveTo(30, -75, -20, -60, 0, 0)
.restore();
// fill the blue sky on the main canvas
ctx.fillStyle='skyblue';
ctx.fillRect(0,0,canvas.width,canvas.height);
// draw the cloud on the main canvas
path.definePath(ctx);
ctx.fillStyle='white';
ctx.fill();
ctx.strokeStyle='black';
ctx.lineWidth=2;
ctx.stroke();
// draw the cloud's silver lining on the temp canvas
path.definePath(ctx1);
ctx1.lineWidth=20;
ctx1.strokeStyle='silver';
ctx1.stroke();
ctx1.globalCompositeOperation='destination-out';
ctx1.fill();
// draw the silver lining onto the main canvas
ctx.drawImage(canvas1,0,0);
body{ background-color: ivory; }
canvas{border:1px solid red;}
<h4>Main canvas with original white cloud + small black stroke<br>The "outside silver lining" is from the temp canvas</h4>
<canvas id="canvas" width=300 height=300></canvas>
<h4>Temporary canvas used to create the "outside stroke"</h4>
<canvas id="canvas1" width=300 height=300></canvas>

Categories