I'm currently working on web app for photo editing using FabricJS and one of features I need to implement is something like Clipping masks from Photoshop.
For example I have this assets: frame, mask and image. I need to insert image inside frame and clip it with mask. Most tricky part is in requirements:
User should be able to modify image inside frame, e.g. move, rotate, skew... Frame itself also can be moved inside canvas.
Number of layers is not limited so user can add objects under or above masked image.
Masks, frames and images is not predefined, user should be able to upload and use new assets.
My current solution is this:
Load assets
Set globalCompositeOperation of image to source-out
Set clipTo function for image.
Add assets on canvas as a group
In this solution clipTo function preserve image inside rectangular area of frame and with help of globalCompositeOperation I'm clipping image to actual mask. At first sight it works fine but if I add new layer above this newly added group it will be cutted off because of globalCompositeOperation="source-out" rule. I've created JSFiddle to show this.
So, that else could I try? I've seen some posts on StackOverflow with advices to use SVGs for clipping mask, but if I understand it correctly SVG must contain only one path. This could be a problem because of third requirement of my app.
Any advice in right direction will help, because right now I'm totally stuck with this problem.
You can do this by using ClipPath property of Img Object which you want to mask. With this, you can Mask Any Type of Object. and also you need to add some Ctx Configuration in ClipTo function of Img Object.
check this link https://jsfiddle.net/naimsajjad/8w7hye2v/8/
(function() {
var img01URL = 'http://fabricjs.com/assets/printio.png';
var img02URL = 'http://fabricjs.com/lib/pug.jpg';
var img03URL = 'http://fabricjs.com/assets/ladybug.png';
var img03URL = 'http://fabricjs.com/assets/ladybug.png';
var canvas = new fabric.Canvas('c');
canvas.backgroundColor = "red";
canvas.setHeight(500);
canvas.setWidth(500);
canvas.setZoom(1)
var circle = new fabric.Circle({radius: 40, top: 50, left: 50, fixed: true, fill: '', stroke: '1' });
canvas.add(circle);
canvas.renderAll();
fabric.Image.fromURL(img01URL, function(oImg) {
oImg.scale(.25);
oImg.left = 10;
oImg.top = 10;
oImg.clipPath = circle;
oImg.clipTo = function(ctx) {
clipObject(this,ctx)
}
canvas.add(oImg);
canvas.renderAll();
});
var bili = new fabric.Path('M85.6,606.2c-13.2,54.5-3.9,95.7,23.3,130.7c27.2,35-3.1,55.2-25.7,66.1C60.7,814,52.2,821,50.6,836.5c-1.6,15.6,19.5,76.3,29.6,86.4c10.1,10.1,32.7,31.9,47.5,54.5c14.8,22.6,34.2,7.8,34.2,7.8c14,10.9,28,0,28,0c24.9,11.7,39.7-4.7,39.7-4.7c12.4-14.8-14-30.3-14-30.3c-16.3-28.8-28.8-5.4-33.5-11.7s-8.6-7-33.5-35.8c-24.9-28.8,39.7-19.5,62.2-24.9c22.6-5.4,65.4-34.2,65.4-34.2c0,34.2,11.7,28.8,28.8,46.7c17.1,17.9,24.9,29.6,47.5,38.9c22.6,9.3,33.5,7.8,53.7,21c20.2,13.2,62.2,10.9,62.2,10.9c18.7,6.2,36.6,0,36.6,0c45.1,0,26.5-15.6,10.1-36.6c-16.3-21-49-3.1-63.8-13.2c-14.8-10.1-51.4-25.7-70-36.6c-18.7-10.9,0-30.3,0-48.2c0-17.9,14-31.9,14-31.9h72.4c0,0,56-3.9,70.8,26.5c14.8,30.3,37.3,36.6,38.1,52.9c0.8,16.3-13.2,17.9-13.2,17.9c-31.1-8.6-31.9,41.2-31.9,41.2c38.1,50.6,112-21,112-21c85.6-7.8,79.4-133.8,79.4-133.8c17.1-12.4,44.4-45.1,62.2-74.7c17.9-29.6,68.5-52.1,113.6-30.3c45.1,21.8,52.9-14.8,52.9-14.8c15.6,2.3,20.2-17.9,20.2-17.9c20.2-22.6-15.6-28-16.3-84c-0.8-56-47.5-66.1-45.1-82.5c2.3-16.3,49.8-68.5,38.1-63.8c-10.2,4.1-53,25.3-63.7,30.7c-0.4-1.4-1.1-3.4-2.5-6.6c-6.2-14-74.7,30.3-74.7,30.3s-108.5,64.2-129.6,68.9c-21,4.7-18.7-9.3-44.3-7c-25.7,2.3-38.5,4.7-154.1-44.4c-115.6-49-326,29.8-326,29.8s-168.1-267.9-28-383.4C265.8,13,78.4-83.3,32.9,168.8C-12.6,420.9,98.9,551.7,85.6,606.2z',{top: 0, left: 180, fixed: true, fill: 'white', stroke: '', scaleX: 0.2, scaleY: 0.2 });
canvas.add(bili);
canvas.renderAll();
fabric.Image.fromURL(img02URL, function(oImg) {
oImg.scale(0.5);
oImg.left = 180;
oImg.top = 0;
oImg.clipPath = bili;
oImg.clipTo = function(ctx) {
clipObject(this,ctx)
}
canvas.add(oImg);
canvas.renderAll();
});
function clipObject(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);
}
}
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<canvas id="c" width="400" height="400"></canvas>
Not sure what you want.
If you want the last image loaded (named img2), the one you send to the back to not effect the layers above do the following.
You have mask,frame,img, and img2;
Put them in the following order and with the following comp settings.
img2, source-over
img, source-over
mask, destination-out
frame, source-over
If you want something else you will have to explain it in more detail.
Personally when I provide masking to the client I give them full access to all the composite methods and allow them to work out what they need to do to achieve a desired effect. Providing a UI that allows you to change the comp setting, and layer order makes it a lot easier to sort out the sometimes confusing canvas composite rules.
I'd suggest looking at this solution.
Multiple clipping areas on Fabric.js canvas
You end up with a shape layer that is used to define the mask shape. That shape then gets applied as a clipTo to your image.
The one limitation I can think off though that you might run into is when you start to rotate various shapes. I know I have it working great with a rectangle and a circle, however ran into some issues with polygons from what I recall... This was all setup under and older version of FabricJS however, so there may have been some improvements there that I'm not experienced with.
The other issue I ran into was drop shadows didn't render correctly when passed to a NodeJS server running FabricJS.
I'm trying to create some little webcam tool that let's you take snapshots with an overlay. Now I've been working with the html5 canvas and got the webcam working. But whenever I try to save the webcam snapshot it just only loads either the webcam snapshot or the overlay and never both.
I've been trying out a couple things this is the closest I could get:
var teaser = new Image();
teaser.src = "http://www.fillmurray.com/g/1200/1200";
teaser.onload = function() {
context.drawImage(teaser, 0, 0);
};
context.drawImage(v, teaser, 0 , 0, w, h); // this only draws the image
// and
context.drawImage(v, 0 , 0, w, h); // this just draws the webcam snapshot
Does anyone know if this is even possible or if I'm just being a complete idiot? So what I want to achieve is to draw two images in 1 canvas and to let the overlay to be on top of the video.
Here is a jsfiddle of what I have so far: http://jsfiddle.net/Px8g3/
You should do this :
context.globalCompositionOperation = "source-atop"
before drawing (or after the 1st draw, try both)
To be more precise :
var teaser = new Image();
teaser.src = "http://www.fillmurray.com/g/1200/1200";
teaser.onload = function() {
context.drawImage(teaser, 0, 0);
};
//context.save(); Try with and without
context.globalCompositionOperation = "source-atop";
context.drawImage(v, 0 , 0, w, h); // Don't know what v is, but i guess it's an image
//context.restore(); Same
To be precise, this make the canvas to be in "source-atop" mode.
What does it means ?
It means that where the top layer (the 2nd image you draw) is transparent, it will keep the background (video) and draw it. Elsewhere, it will draw your 2nd layer
I write this html code :
<div id="container">
<canvas id="imageView" width="1181" height="1181">
<p>Unfortunately, your browser is currently unsupported by our web
application.</p>
</canvas>
<script type="text/javascript">
window.onload = function() {
var c = document.getElementById('imageView');
var cxt = c.getContext('2d');
var img = new Image();
img.src = "map.jpg";
cxt.drawImage(img, 0, 0);
};
</script>
</div>
And write this javascript :
this.mousemove = function(ev) {
if (!tool.started) {
return;
}
var x = Math.min(ev._x, tool.x0),
y = Math.min(ev._y, tool.y0),
w = Math.abs(ev._x - tool.x0),
h = Math.abs(ev._y - tool.y0);
context.clearRect(0, 0, canvas.width, canvas.height);
if (!w || !h) {
return;
}
context.clearRect(x, y, w, h);
context.strokeRect(x, y, w, h);
this code is make a rectangle . I want change this rectangle to a area map , that when I click on the area do something , (for example open google.com) .
If I understand you correctly you want to invoke a function when you hit a pixel on the actual map - not just in the map area.
Method 1
You can check a map click in more than one way. You can simply check for the pixel value at the click point to check if it is inside the area you want it to be by comparing the map color value.
I provided an example below for this method.
Method 2
You can pre-define a polygon which traces the outline of the map area you want to check.
Then build a path (ctx.beginPath(); and ctx.lineTo(..); etc.) to allow the use of the method:
if (ctx.isPointInPath(x, y)) { ... };
This is a good method if you have small regions to check.
Method 3
Store a separate image of the map containing only a matte (sort of an alpha map), That is usually black (or transparent) for non-clickable areas, white for clickable areas.
This is useful if your map is complex color-wise and a simple pixel value check is not trivial.
And speaking of which: you can even provide different solid color values for different areas so that you can define red color = USA, blue = Argentina, etc. As these are not visible to the user the only thing that matters is that the color value can be recognized (for this reason don't save images for this use with an ICC color profile).
Then project the mouse position from the click onto the matte image (which is basically an off-screen canvas where the matte image is drawn into) and check for the color (white or other color).
Example for method 1
This is a simple example, but in any case there are a couple of things you need to know in advance:
That the image is loaded from same server as the page or from a domain that allow cross-origin use. Or else you cannot grab a pixel from the map due to security reasons.
You need to know what color or alpha value to check for. If the map is solid and everything is transparent you just need to check for alpha value above zero (as in this example), and if not just check the RGB value of the region you want to trigger an action with.
ONLINE DEMO HERE
HTML:
<canvas width=725 height=420 id="demo"></canvas>
JavaScript:
var ctx = demo.getContext('2d'),
img = new Image();
/// we need to wait for the image to actually load:
img.onload = function() {
/// image is loaded and we can raw it onto canvas
ctx.drawImage(this, 0, 0);
/// enable mouse click
demo.onclick = function(e) {
/// adjust mouse position to be relative to canvas
var rect = demo.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
/// grab a pixel
var data = ctx.getImageData(x, y, 1, 1).data;
/// check it's alpha value to see if we're in a map point
/// this of course assumes the map has transparent areas.
/// if not just check for the color values instead.
if (data[3] > 0) alert('We hit map');
}
}
/// we need crossOrigin allowed image or we can't grab pixel later
img.crossOrigin = 'anonymous';
img.src = 'http://i.imgur.com/x8Ap3ij.png';
Just replace the alert with:
window.open('http://google.com/');
if you want it to open a new window/tab.
You can turn canvas into an anchor link by using addEventListener to listen for clicks on the canvas.
Then you can use window.open to open google in a new browser tab.
Also, you need to use image.onload to give your image time to load before using drawing it.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var img=new Image();
img.onload=function(){
ctx.drawImage(img,0,0);
canvas.addEventListener("click",function(){
window.open("http://google.com");
});
}
img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/google.jpg";
I have a pixel-array and I would like to draw it onto the canvas, without using the putImageData, because that function ignores clip data.
So, just to make this work i've converted the pixel-array to a data-url, and then appended the url to an image which I then draw onto the canvas, like this:
var ctx = {
display: document.querySelector('canvas#display').getContext('2d'),
convert: document.querySelector('canvas#convert').getContext('2d')
}
var image_data = THE_IMAGE_DATA_THAT_I_ALREADY_HAVE;
ctx.convert.putImageData(image_data, 0, 0);
var image_data_URL = ctx.convert.canvas.toDataURL('image/png');
var converter_image = document.querySelector('img#converter-image');
converter_image.onload = function () {
ctx.display.save();
ctx.display.beginPath();
ctx.display.arc(320, 240, 240, 0, Math.PI*2, true);
ctx.display.clip();
ctx.display.drawImage(converter_image, 0, 0, 640, 480);
ctx.display.restore();
}
converter_image.src = image_data_URL;
However, this is gives a REALLY bad performance, especially since I want to do this 60/sec.
There must be another way, right?
A work around will be to draw your pixel using getImageData / putImageData in an offscreen canvas then drawing the offscreen canvas back in your canvas.
More infos :
Canvas offscreen
Pixel drawing with putImagedata (click on the blank in top of the code to launch it)
Currently there are are two images I would like to rotate on the canvas, I tried save and restore but didn't work
function SetCanvas()
{
var canvas = document.getElementById('pic1');
if(canvas.getContext)
{
var ctx = canvas.getContext('2d');
// ctx.save();
ctx.rotate(0.5);
var image = new Image();
image.src ='ME.JPG';
image.onload = function(){
ctx.drawImage(image, 90,0,200,100);
};
}
//ctx.restore();
var canvas2 = document.getElementById("pic2");
var image2 = new Image();
image2.src = 'ME2.JPG';
if(canvas2.getContext)
{
image2.onload = function(){
ctx2=canvas2.getContext('2d');
ctx2.drawImage(image2, 0,0,200,100);
};
}
}
<ul id="picsCanvas" style="overflow:hidden;white-space:nowrap; list-style-type:none;">
<li style=" display:inline; float:left" id="first">
<canvas ID="pic1" width="300" height="360" ></canvas>
</li>
<li id="second" style="margin-top:0px; display:inline; float:left; position:absolute ">
<canvas id="pic2" width="300" height="360" style="position:absolute" ></canvas>
</li>
</ul>
Please note that the code might not be correct as it is something I did a while ago, I just want to get an idea of how to do it and if it is possible... thanks for your help.
The images are loading asynchronously. This means that that entire function (minus the onload handlers for the images happens first. Then when the images are loaded, their handlers are called. This happens in a second pass. By the time this happens, you already rotated and restored the canvas, effectively wiping the rotation out.
The simple fix is to rotate and restore the canvas inside each of the image onload handlers.
These two links give a pretty good explanation and example of how to rotate with HTML5 canvas
https://developer.mozilla.org/en/Drawing_Graphics_with_Canvas
https://developer.mozilla.org/en/Canvas_tutorial/Basic_animations
You set the different angles when you rotate (see code example below).
The general gist of it is:
1) save the context
2) transform to, usually, the center of the image
3) rotate
4) transform back
5) draw image
6) restore
In your case, with two images, you need to transform the origin to the second image before you make the second rotation call. Below is a simplified example rotating one image. Get that sorted and then make the second transform/rotate.
Example:
var canvas = document.getElementById("yourCanvas");
var ctx = canvas.getContext("2d");
var angle = 0;
window.setInterval(function(){
angle = angle+1;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.fillStyle = "#FF0000";
// first image
ctx.translate(150,200);
ctx.rotate( angle*Math.PI/180 ); // rotate 90 degrees
ctx.translate(-150,-200);
ctx.fillStyle = "black";
ctx.fillRect(100, 150, 100, 100);
ctx.fill();
ctx.restore();
}, 5);