Fabric.js demos · Erasing with Eraser Brush

This demo shows how to use the EraserBrush to achieve erasing.

Custom Build

Erasing is not part of fabric’s default build. You will need to create a custom build.

Usage

The same as PencilBrush


  canvas.freeDrawingBrush = new fabric.EraserBrush(canvas);
  //  optional
  canvas.freeDrawingBrush.width = 10;
  canvas.isDrawingMode = true;

erasable property

The object’s erasable property (boolean | 'deep') is used to determine if it is erasable or not. By default it is set to true.

  //  set in options
  const obj = new fabric.Rect({ erasable: false });
  //  change
  obj.set('erasable', true);

  //  make all objects not erasable by default
  fabric.Object.prototype.erasable = false;

erasable property and fabric.Group

The deep option introduces fine grained control over a group’s erasable property.

Setting a non-group object’s erasable property to deep is the same as setting it to true.

  //  handling group `erasable` config
  //  bad - set all groups to be `deep` by default (not suggested)
  fabric.Group.prototype.erasable = 'deep';
  //  good - set directly
  const group = new fabric.Group({ erasable: 'deep' });

  //  remove childObj from the group, eraser will be propagated to it
  group.removeWithUpdate(childObj);
  childObj.getEraser();
  //  remove eraser from object
  delete childObj.clipPath;

  //  remove childObj from the group, disabling eraser propagation
  group.erasable = 'deep';
  group.removeWithUpdate(childObj);
  group.erasable = true;

  //  setting a non-group object's `erasable` property, both have the same effect
  obj.set('erasable', true);
  obj.set('erasable', 'deep');

Erasing and the clipPath

If you’re using clipPath and erasing you should read this carefully.

The eraser mixin uses the clipPath property of an object the apply erasing to it.

  1. It creates a fabric.Group object and assigns it to the object’s clipPath.
  2. It creates a fabric.Rect and adds it to the group as the first object for clipping to be successful (acts as the background for destination-out global composition operation).
  3. It applies the existing clipPath property to the fabric.Rect from the previous step.
  4. After erasing is done it adds fabric.Paths to create an erasing effect on the object.

This means that if you set the clipPath after erasing an object, erasing will be lost.

However, you could simply add and remove objects from the clipPath as you would with any fabric.Group.

For conveince the mixin exposes the following methods on fabric.Object:

  1. getEraser returns the clipPath (fabric.Group) if the object was erased or undefined.
  2. getClipPath returns the original clipPath regardless of the mixin’s changes or undefined.
const originalClipPath = obj.getClipPath();
const eraser = obj.getEraser();
if(eraser) {
  //  `obj` was erased, `eraser` is `fabric.Group`, `obj.clipPath === eraser`
  const first = eraser._objects[0];
  //  `first` is `fabric.Rect`
  //  removing `first` from the group will make erasing invisible
  //  if `obj` had a clipPath before erasing then `first.clipPath === originalClipPath`

  const eraserPaths = eraser.getObjects('path');
} else {
  //  `clipPath === originalClipPath`
  const clipPath = obj.clipPath;
}

Erasing and Canvas#backgroundColor, Canvas#overlayColor

Erasing uses fabric.Object#clipPath under the hood.

backgroundColor and overlayColor don’t extend fabric.Object, hence they can’t be erased.

A simple workaround would be adding a rect at the bottom/top of the object stack that could behave as backgroundColor or overlayColor.

Example

  
erasing:end
N/A
  button.active {
    background: limegreen;
    font-weight: bold
  }
function changeAction(target) {
    ['select','erase','draw','spray'].forEach(action => {
      const t = document.getElementById(action);
      t.classList.remove('active');
    });
  if(typeof target==='string') target = document.getElementById(target);
    target.classList.add('active');
    switch (target.id) {
      case "select":
        canvas.isDrawingMode = false;
        break;
      case "erase":
        canvas.freeDrawingBrush = new fabric.EraserBrush(canvas);
        canvas.freeDrawingBrush.width = 10;
        canvas.isDrawingMode = true;
        break;
      case "draw":
        canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);
        canvas.freeDrawingBrush.width = 35;
        canvas.isDrawingMode = true;
        break;
      case "spray":
        canvas.freeDrawingBrush = new fabric.SprayBrush(canvas);
        canvas.freeDrawingBrush.width = 35;
        canvas.isDrawingMode = true;
        break;
      default:
        break;
    }
  }
  function init() {
    canvas.setOverlayColor("rgba(0,0,255,0.4)",undefined,{erasable:false});
    const t = new fabric.Triangle({
      top: 300,
      left: 210,
      width: 100,
      height: 100,
      fill: "blue",
      erasable: false
    });

    canvas.add(
      new fabric.Rect({
        top: 50,
        left: 100,
        width: 50,
        height: 50,
        fill: "#f55",
        opacity: 0.8
      }),
      new fabric.Rect({
        top: 50,
        left: 150,
        width: 50,
        height: 50,
        fill: "#f55",
        opacity: 0.8
      }),
      new fabric.Group([
        t,
        new fabric.Circle({ top: 140, left: 230, radius: 75, fill: "green" })
      ], { erasable: 'deep' })
    );
 fabric.Image.fromURL('https://ip.webmasterapi.com/api/imageproxy/http://fabricjs.com/assets/mononoke.jpg',
      function (img) {
        // img.set("erasable", false);
        img.scaleToWidth(480);
        img.clone((img) => {
          canvas.add(
            img
              .set({
                left: 400,
                top: 350,
                clipPath: new fabric.Circle({
                  radius: 200,
                  originX: "center",
                  originY: "center"
                }),
                angle: 30
              })
              .scale(0.25)
          );
          canvas.renderAll();
        });

        img.set({ opacity: 0.7 });
        function animate() {
          img.animate("opacity", img.get("opacity") === 0.7 ? 0.4 : 0.7, {
            duration: 1000,
            onChange: canvas.renderAll.bind(canvas),
            onComplete: animate
          });
        }
        animate();
        canvas.setBackgroundImage(img);
        img.set({ erasable:false });
        canvas.on("erasing:end", ({ targets, drawables }) => {
          var output = document.getElementById("output");
          output.innerHTML = JSON.stringify({
            objects: targets.map((t) => t.type),
            drawables: Object.keys(drawables)
          }, null, '\t');
        })
        canvas.renderAll();
      },
      { crossOrigin: "anonymous" }
    );

    function animate() {
      try {
        canvas
          .item(0)
          .animate("top", canvas.item(0).get("top") === 500 ? "100" : "500", {
            duration: 1000,
            onChange: canvas.renderAll.bind(canvas),
            onComplete: animate
          });
      } catch (error) {
        setTimeout(animate, 500);
      }
    }
    animate();
  }

  const setDrawableErasableProp = (drawable, value) => {
    canvas.get(drawable)?.set({ erasable: value });
    changeAction('erase');
  };

  const setBgImageErasableProp = (input) =>
    setDrawableErasableProp("backgroundImage", input.checked);

  const downloadImage = () => {
    const ext = "png";
    const base64 = canvas.toDataURL({
      format: ext,
      enableRetinaScaling: true
    });
    const link = document.createElement("a");
    link.href = base64;
    link.download = `eraser_example.${ext}`;
    link.click();
  };

  const downloadSVG = () => {
    const svg = canvas.toSVG();
    const a = document.createElement("a");
    const blob = new Blob([svg], { type: "image/svg+xml" });
    const blobURL = URL.createObjectURL(blob);
    a.href = blobURL;
    a.download = "eraser_example.svg";
    a.click();
    URL.revokeObjectURL(blobURL);
  };

  const toJSON = async () => {
    const json = canvas.toDatalessJSON(["clipPath"]);
    const out = JSON.stringify(json, null, "\t");
    const blob = new Blob([out], { type: "text/plain" });
    const clipboardItemData = { [blob.type]: blob };
    try {
      navigator.clipboard &&
        (await navigator.clipboard.write([
          new ClipboardItem(clipboardItemData)
        ]));
    } catch (error) {
      console.log(error);
    }
    const blobURL = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = blobURL;
    a.download = "eraser_example.json";
    a.click();
    URL.revokeObjectURL(blobURL);
  };
  const canvas = this.__canvas = new fabric.Canvas('c');
  init();
  changeAction('erase');