Mask or clip given polygon in FabricJS
Asked Answered
V

1

7

I have a background and an unlimited amount of images or animations where I'd like to mask or clip part of the image or animation to the background. I'd like the user to use or move around a polygon to determine where they would like this to happen. See JSFiddle

I've tried a couple things but I seem to have to re-add the background image multiple times to get the effect I'm looking for but even at that, it's not just filling the polygon. I'd the user to be able to move the polygon around and resize the shapes or animations and have the mask or clip effect still work.

Any help would be appreciated!

var canvas = new fabric.Canvas('c');

var points = [{
    x: 3,
    y: 4,
  },
  {
    x: 16,
    y: 3,
  },
  {
    x: 30,
    y: 5,
  },
  {
    x: 25,
    y: 55,
  },
];

// define a function that can locate the controls.
// this function will be used both for drawing and for interaction.
function polygonPositionHandler(dim, finalMatrix, fabricObject) {
  var x =
    fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x,
    y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y;
  return fabric.util.transformPoint({
      x: x,
      y: y
    },
    fabric.util.multiplyTransformMatrices(
      fabricObject.canvas.viewportTransform,
      fabricObject.calcTransformMatrix()
    )
  );
}

function getObjectSizeWithStroke(object) {
  var stroke = new fabric.Point(
    object.strokeUniform ? 1 / object.scaleX : 1,
    object.strokeUniform ? 1 / object.scaleY : 1
  ).multiply(object.strokeWidth);
  return new fabric.Point(
    object.width + stroke.x,
    object.height + stroke.y
  );
}

// define a function that will define what the control does
// this function will be called on every mouse move after a control has been
// clicked and is being dragged.
// The function receive as argument the mouse event, the current trasnform object
// and the current position in canvas coordinate
// transform.target is a reference to the current object being transformed,
function actionHandler(eventData, transform, x, y) {
  var polygon = transform.target,
    currentControl = polygon.controls[polygon.__corner],
    mouseLocalPosition = polygon.toLocalPoint(
      new fabric.Point(x, y),
      "center",
      "center"
    ),
    polygonBaseSize = getObjectSizeWithStroke(polygon),
    size = polygon._getTransformedDimensions(0, 0),
    finalPointPosition = {
      x: (mouseLocalPosition.x * polygonBaseSize.x) / size.x +
        polygon.pathOffset.x,
      y: (mouseLocalPosition.y * polygonBaseSize.y) / size.y +
        polygon.pathOffset.y,
    };
  polygon.points[currentControl.pointIndex] = finalPointPosition;
  return true;
}

// define a function that can keep the polygon in the same position when we change its
// width/height/top/left.
function anchorWrapper(anchorIndex, fn) {
  return function(eventData, transform, x, y) {
    var fabricObject = transform.target,
      absolutePoint = fabric.util.transformPoint({
          x: fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x,
          y: fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y,
        },
        fabricObject.calcTransformMatrix()
      ),
      actionPerformed = fn(eventData, transform, x, y),
      newDim = fabricObject._setPositionDimensions({}),
      polygonBaseSize = getObjectSizeWithStroke(fabricObject),
      newX =
      (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) /
      polygonBaseSize.x,
      newY =
      (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) /
      polygonBaseSize.y;
    fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
    return actionPerformed;
  };
}

var polygon = new fabric.Polygon(points, {
  left: 150,
  top: 100,
  fill: "transparent",
  strokeWidth: 2,
  stroke: "green",
  scaleX: 2,
  scaleY: 2,
  objectCaching: false,
  transparentCorners: false,
  cornerColor: "blue",
  absolutePositioned: true,
});

fabric.Image.fromURL("https://s3.amazonaws.com/static-mywoodhome/wp-content/uploads/sites/4/2014/11/Screen-Shot-2016-10-18-at-4.39.30-PM.png", (img) => {
  canvas.add(img);
  img.sendToBack();
});

fabric.Image.fromURL("https://s3.amazonaws.com/static-mywoodhome/wp-content/uploads/sites/4/2014/11/Screen-Shot-2016-10-18-at-4.39.30-PM.png", (house) => {
  fabric.Image.fromURL("https://www.onlygfx.com/wp-content/uploads/2017/06/man-silhouette-1-79x300.png", (person) => {
    person.set("left", 100);
    canvas.add(person, polygon);
    house.set({
      left: 10,
      top: 10,
      clipPath: polygon,
    });
  });
});

var poly = polygon;

var lastControl = poly.points.length - 1;
poly.cornerStyle = "circle";
poly.cornerColor = "rgba(0,0,255,0.5)";
poly.controls = poly.points.reduce(function(acc, point, index) {
  acc["p" + index] = new fabric.Control({
    positionHandler: polygonPositionHandler,
    actionHandler: anchorWrapper(
      index > 0 ? index - 1 : lastControl,
      actionHandler
    ),
    actionName: "modifyPolygon",
    pointIndex: index,
  });
  return acc;
}, {});

Given the example, I'd like an output like this:

enter image description here

Vocalist answered 29/4, 2024 at 15:15 Comment(0)
N
0

Sad truth, but we have to accept

I made a simple example out of circle, triangle, with static html,js. Got to the point of masking background by union of(circle, triangle). Just have to change the union part to intersection part

Yes, there is some glitches in below code

  • The triangle moves in vectorially opposite direction to that when user moves circle (something related to bounding box, center)
  • So you can overlap circle and triangle by selecting circle(like selecting a group of files) and moving it around (in top left corner alone)

So why am I posting this answer halfway?

  • The answer is not the code really, but it is that intersection of two objects is not available in FabricJs, so you can't technically do it.
  • I have used fabric.Group,out of circle and triangle to make the union mask
  • But for intersection mask, we need to get a fabric object of intersection, there is a method new fabric.Intersection() which can take in fabric points, to make things easy it has intersectLineLine intersectLinePolygon intersectPolygonPolygon intersectPolygonRectangle

But there is no way for you to get your silhouette image object, and polygon intersection in library (you can check if they intersect, but can't make mask out of it) As for your question,since you cant get intersection of objects,you can use it as mask, unless you get out of the object-svg framework of fabric into raster graphics. Or use other library which supports intersection of two objects(boolean operations on objects)

Checkout answer by Christoph in How to fill the intersection of two objects in Fabric.js?

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Fabric.js Example</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js"></script>

  <style>
    canvas {
      border: 1px solid #ccc;
    }
  </style>
</head>

<body>
  <canvas id="canvas" width="800" height="600"></canvas>
  <script>
    // Create a Fabric.js canvas
    const canvas = new fabric.Canvas('canvas');
    canvas.backgroundColor = '#45eeee';
    canvas.renderAll()// Create a circle and a triangle
      const circle = new fabric.Circle({
        radius: 100,
        top: 0,
        left: 0,
        originX: 'center',
        originY: 'center'
      });

      const triangle = new fabric.Triangle({
        width: 150,
        height: 150,
        top: 50,
        left: 0,
        originX: 'center',
        originY: 'center'
      }); canvas.add(circle); canvas.add(triangle);

    // Load an image onto the canvas
    fabric.Image.fromURL('https://picsum.photos/seed/picsum/1600/1200', function (img) {
      img.originX = 'center';
      img.originY = 'center';
      // Resize the image if necessary
      img.scaleToWidth(1600);
      img.scaleToHeight(1200);
      img.selectable = false;
      img.hasControls = false;

      

      // Group the circle and triangle
      function update() {
        maskGroup = new fabric.Group([circle,
        triangle], {
          originX: 'center',
          originY: 'center',
        });

        // Apply the mask as a clipPath to the image
        img.set({
          clipPath: maskGroup
        });

        // Add the image to the canvas
        canvas.add(img);
        canvas.renderAll()
      }
      update();
      circle.on('moving', update);
      circle.on('modified', update);
      triangle.on('moving', update);
      triangle.on('modified', update);
    });

  </script>
</body>

</html>
Noshow answered 6/10, 2024 at 11:28 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.