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.GroupThe 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#overlayColorbackgroundColor 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/https://bombcowpat.github.io/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');