This demo shows how to use the EraserBrush
to achieve erasing.
Erasing is not part of fabric’s default build. You will need to create a custom build.
// same as `PencilBrush`
canvas.freeDrawingBrush = new fabric.EraserBrush(canvas);
canvas.isDrawingMode = true;
// optional
canvas.freeDrawingBrush.width = 10;
// undo erasing
canvas.freeDrawingBrush.inverted = true;
erasable
propertyThe 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.
deep
the eraser will erase nested objects if they are erasable, leaving the group and its other objects untouched. In case of very large groups, performance will suffer.true
the eraser will erase the entire group.
Once the group changes (an object is added/removed) the eraser is propagated to its children for proper functionality.
If you don’t want this to happen you can set erasable
to deep
before removing the objects and restore its value afterwards.false
the eraser will leave all objects including the group untouched.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
propertyThe 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;
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.
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();
Start (mousedown
):
canvas fires an erasing:start
event.
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
.
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.
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://fabric5.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');