0

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:

  1. User should be able to modify image inside frame, e.g. move, rotate, skew... Frame itself also can be moved inside canvas.
  2. Number of layers is not limited so user can add objects under or above masked image.
  3. Masks, frames and images is not predefined, user should be able to upload and use new assets.

My current solution is this:

  1. Load assets
  2. Set globalCompositeOperation of image to source-out
  3. Set clipTo function for image.
  4. 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.

Progresso
  • 1
  • 1
  • 2

3 Answers3

3

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>
0

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.

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • JSFiddle that I provided is just an example, in my case all elements in canvas is created by user so there can be img3, img4...imgN or even another group of img, mask and frame so manual override of layer order or comp settings is not the best solution. However I never thought about giving an option to change comp settings for clients. In my app I have UI to change layer order but I'm worry that with addition of comp settings it will become too confusing. Can you describe how do you deal with confusion between layer order and comp settings in your UI? – Progresso Mar 22 '16 at 07:21
  • Do not underestimate the user's willingness to learn how to use a good app. A good UI needs effortless access to help, it should be no more than a single keystroke (F1) or button click away, it should be contextually aware (tool tips), and unobtrusive (does not block UI access visually or functionally), it starts with minimal description but provide additional means of accessing information without interruption to workflow. As a coder / designer, you stop user confusion by understanding your app before you create it. The confusion is currently not the user it seems to me. – Blindman67 Mar 22 '16 at 08:09
0

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.

Community
  • 1
  • 1
PromInc
  • 1,174
  • 7
  • 12
  • In my case masks stored in PNG format. I can save them as SVGs to use in clipTo, but as I know clipTo function can work only if SVG consist of one path. – Progresso Mar 22 '16 at 07:40
  • Sadly I believe you are correct about only one path from an SVG. The clipping region functionality of FabricJS seems to be one difficult area to work with that is in some ways a major limitation of the library... – PromInc Mar 23 '16 at 18:17