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

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

  //  undo erasing
  canvas.freeDrawingBrush.inverted = 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 non 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);
  let propagatedEraser = childObj.eraser;

  //  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');

eraser property

The object’s eraser property is an instance of fabric.Eraser. It holds the eraser’s paths.
There’s no need to handle anything here, unless you’re after heavy customization.

  let myEraser = obj.eraser;
  //  remove eraser from object
  delete obj.eraser;
  //  mark as dirty so object gets invalidated in the rendering process
  obj.dirty = true;

Erasing and Canvas#backgroundColor, Canvas#overlayColor

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/overlayColor or use backgroundImage/overlayImage respectively.

Erasing and animating

During interaction, animating objects that are erasable will render strangely. It is advised setting all animating objects’ erasable property to false from within the erasing:start event to avoid this. In future releases it may be supported. If you must support it, you will need to manually update the mask created by the brush on each step of the animation. Performance may suffer significantly.

  eraserBrush.preparePattern();
  eraserBrush._render();

Life cycle and Events

  1. Start (mousedown):
    canvas fires an erasing:start event.

  2. Interaction (mousemove):
    main context is clipped by the brush, top context is masked with relevant objects to achieve the effect of erasing only what’s erasable.

  3. End (mouseup):
    canvas fires a path:created:before event. If in need, this is a good place to customize the path before it’s propagated to all objects in stake.
    The erasers of objects in stake (erasable objects that intersect with the created path) are updated.
    In this process every object that is mutated will fire an erasing:end event.
    After iteration is done canvas fires an erasing:end event followed by a path:created event.
    This concludes the erasing cycle.

Example

  
erasing:end
N/A
  button.active {
    background: limegreen;
    font-weight: bold
  }
let erasingRemovesErasedObjects = false;
function changeAction(target) {
    ['select','erase','undo','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 "undo":
        canvas.freeDrawingBrush = new fabric.EraserBrush(canvas);
        canvas.freeDrawingBrush.width = 10;
        canvas.freeDrawingBrush.inverted = true;
        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');
          if(erasingRemovesErasedObjects) {
            targets.forEach(obj => obj.group?.removeWithUpdate(obj) || canvas.remove(obj));
          }
        })
        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 setErasingRemovesErasedObjects = (input) =>
    (erasingRemovesErasedObjects = 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", "eraser"]);
    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');