canvasfabricjsglobalcompositeoperation

Adding a border on top of globalCompositeOperation source-atop


I'm working on a problem where I want to enable a user to upload an image which will be displayed inside a pre-defined shape, and any part of the uploaded image outside of the shape will be cut out.

In this example I'm working with a circle, and one of the requirements is for the circle to have a visible border however when I set the globalCompositeOperation: "source-atop" the border of the circle is also covered by the image. I though that maybe adding another slightly bigger circle around the original circle after the image is added to the canvas might solve the problem but no luck. The code below uses fabricJs

const initBaseCanvas = (imageSize) => {
    const container = document.getElementById("customizer-container");
    const containerWidth = container.offsetWidth;
    const containerHeight = container.offsetHeight;

    // Create base canvas
    const initCanvas = new fabric.Canvas("base-image", {
      width: containerWidth,
      height: containerHeight,
      selectable: false,
      evented: false,
      allowTouchScrolling: true,
      backgroundColor: "transparent",
    });

    // Create the image boundary
    const circle = new fabric.Circle({
      radius: imageSize / 2,
      fill: "",
      stroke: "transparent",
      backgroundColor: "transparent",
      fill: "#f9f9f9",
      strokeWidth: 1,
      selectable: false,
      evented: false,
    });
    initCanvas.add(circle);
    initCanvas.centerObject(circle);

    // Insert uploaded image in the center of the circle and pre-select
    new fabric.Image.fromURL(
      URL.createObjectURL(uploadedFile),
      (img) => {
        // Scale image down if bigger than canvas to ensure bounding box is visible
        const imgWidht = img.width;
        if (!imgWidht || imgWidht >= containerWidth) {
          img.scaleToWidth(containerWidth - 50);
        }

        initCanvas.add(img);
        initCanvas.centerObject(img);
        initCanvas.renderAll();
        initCanvas.setActiveObject(img);

        const circle2 = new fabric.Circle({
          radius: imageSize / 2 + 1,
          fill: "transparent",
          stroke: "#fd219b",
          backgroundColor: "transparent",
          strokeWidth: 2,
          selectable: false,
          evented: false,
          globalCompositeOperation: "source-over",
        });
        initCanvas.add(circle2);
        initCanvas.centerObject(circle2);
        initCanvas.renderAll();
        console.log(initCanvas.getObjects());
      },
      {
        // The image will be clipped outside of the image boundary
        globalCompositeOperation: "source-atop",

      }
    );

    return initCanvas;
  };

I've tried using all combinations of globalCompositeOperation as well as adding a secondary circle. I also tried adding a separate canvas with the secondary circle but also no luck.


Solution

  • I've found a solution. Im using a circle as a clip path for the image, and then adding a slightly bigger transparent circle for the border. I ended up not having to use the globalCompositeOperation thanks to the clipPath and had to set preserveObjectStacking: true on the canvas to prevent the image (only selectable component) from jumping to the front of the stack.

    /** Initialize base canvas */
    const initBaseCanvas = (imageSize) => {
    const container = document.getElementById("customizer-container");
    const containerWidth = container.offsetWidth;
    const containerHeight = container.offsetHeight;
    
    // Create base canvas
    const initCanvas = new fabric.Canvas("base-image", {
      width: containerWidth,
      height: containerHeight,
      selectable: false,
      evented: false,
      allowTouchScrolling: true,
      backgroundColor: "transparent",
      preserveObjectStacking: true, // Need this to not bring uploaded image to front when moving
    });
    
    // Create the image boundary
    const circle = new fabric.Circle({
      radius: imageSize / 2,
      backgroundColor: "transparent",
      fill: "#f9f9f9",
      selectable: false,
      evented: false,
      absolutePositioned: true,
    });
    initCanvas.add(circle);
    initCanvas.centerObject(circle);
    
    // Insert uploaded image in the center of the circle and pre-select
    const image = new fabric.Image();
    image.clipPath = circle;
    initCanvas.add(image);
    
    image.setSrc(URL.createObjectURL(uploadedFile), (img) => {
      //  Scale image down if bigger than canvas to ensure bounding box is visible
      const imgWidht = img.width;
      if (!imgWidht || imgWidht >= containerWidth) {
        img.scaleToWidth(containerWidth - 50);
      }
    
      initCanvas.centerObject(img);
      initCanvas.setActiveObject(img);
    
      // Colored border
      const circle2 = new fabric.Circle({
        radius: imageSize / 2 + 1,
        stroke: "#fd219b",
        fill: "transparent",
        strokeWidth: 2,
        selectable: false,
        evented: false,
      });
      initCanvas.add(circle2);
      initCanvas.centerObject(circle2);
      initCanvas.getObjects()[2].bringToFront();
      initCanvas.renderAll();
    });
    
    return initCanvas;
    };