Code: https://codepen.io/eajithkumar128/pen/poNwJJv?editors=0010
I have a table like structure in the canvas drawn using a rectangle, I have drawn lines in the row and column ends like below (lines in blue color are rectangle and red color lines are the fabric line object)
I am trying to adjust the size of the rectangles by moving the lines, whenever I am moving the column lines I could see the entire rectangle itself moving wherein I am only trying to update the width, Also I could see an imaginary rectangle drawn behind it which has correct width, but the original rectangle is not resizing correctly. Please refer to the image below.
Code to get the rect object before and after lines while selecting the line:
canvas.on('selection:created', function(e){
let targetEle = e.target;
initialPosition = e.left;
selectedBoxesLeft = []
selectedBoxesRight = []
canvas.forEachObject((v)=>{
if(v.left+v.width === targetEle.left && v.type==="rect"){
selectedBoxesLeft.push(v);
}
if(v.left === targetEle.left && v.type==="rect"){
selectedBoxesRight.push(v);
}
});
console.log(selectedBoxesLeft);
console.log(selectedBoxesRight);
});
Code to resize while moving:
canvas.on('object:moving',function(e){
let target = e.target;
if(target.left < initialPosition){
selectedBoxesLeft.forEach((v)=>{
v.width =target.left - v.left
})
selectedBoxesRight.forEach((v)=>{
let left = v.left
v.left = target.left;
v.width = v.width + (left- target.left)
});
canvas.renderAll()
}else{
// selectedBoxesLeft.forEach((v)=>{
// v.width = v.width + (target.left - v.width)
// })
// selectedBoxesRight.forEach((v)=>{
// // v.width = v.width + (v.left - target.left)
// v.left = target.left;
// v.width = v.width - target.left;
// })
}
});
While Resizing the rectangle instead of using
v.width =target.left - v.left
changed it to
v.set('width', target.left - v.left)
resolved the issue
Related
I'm trying to use Fabric.js with Fabric Brush This issue that I'm running into is that Fabric Brush only puts the brush strokes onto the Top Canvas and not the lower canvas. (The stock brushes in fabric.js save to the bottom canvas) I think I need to convert "this.canvas.contextTop.canvas" to an object and add that object to the the lower canvas. Any ideas?
I've tried running:
this.canvas.add(this.canvas.contextTop)
in
onMouseUp: function (pointer) {this.canvas.add(this.canvas.contextTop)}
But I'm getting the error
Uncaught TypeError: obj._set is not a function
So the contextTop is CanvasHTMLElement context. You cannot add it.
You can add to the fabricJS canvas just fabric.Object derived classes.
Look like is not possible for now.
They draw as pixel effect and then they allow you to export as an image.
Would be nice to extend fabricJS brush interface to create redrawable objects.
As of now with fabricJS and that particular version of fabric brush, the only thing you can do is:
var canvas = new fabric.Canvas(document.getElementById('c'))
canvas.freeDrawingBrush = new fabric.CrayonBrush(canvas, {
width: 70,
opacity: 0.6,
color: "#ff0000"
});
canvas.isDrawingMode = true
canvas.on('mouse:up', function(opt) {
if (canvas.isDrawingMode) {
var c = fabric.util.copyCanvasElement(canvas.upperCanvasEl);
var img = new fabric.Image(c);
canvas.contextTopDirty = true;
canvas.add(img);
canvas.isDrawingMode = false;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<script src="https://tennisonchan.github.io/fabric-brush/bower_components/fabric-brush/dist/fabric-brush.min.js"></script>
<button>Enter free drawing</button>
<canvas id="c" width="500" height="500" ></canvas>
That is just creating an image from the contextTop and add as an object.
I have taken the approach suggested by AndreaBogazzi and modified the Fabric Brush so that it does the transfer from upper to lower canvas (as an image) internal to Fabric Brush. I also used some code I found which crops the image to a smaller bounding box so that is smaller than the full size of the canvas. Each of the brushes in Fabric Brush has an onMouseUp function where the code should be placed. Using the case of the SprayBrush, the original code here was:
onMouseUp: function(pointer) {
},
And it is replaced with this code:
onMouseUp: function(pointer){
function trimbrushandcopytocanvas() {
let ctx = this.canvas.contextTop;
let pixels = ctx.getImageData(0, 0, canvas.upperCanvasEl.width, canvas.upperCanvasEl.height),
l = pixels.data.length,
bound = {
top: null,
left: null,
right: null,
bottom: null
},
x, y;
// Iterate over every pixel to find the highest
// and where it ends on every axis ()
for (let i = 0; i < l; i += 4) {
if (pixels.data[i + 3] !== 0) {
x = (i / 4) % canvas.upperCanvasEl.width;
y = ~~((i / 4) / canvas.upperCanvasEl.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
// Calculate the height and width of the content
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
// generate a second canvas
var renderer = document.createElement('canvas');
renderer.width = trimWidth;
renderer.height = trimHeight;
// render our ImageData on this canvas
renderer.getContext('2d').putImageData(trimmed, 0, 0);
var img = new fabric.Image(renderer,{
scaleY: 1./fabric.devicePixelRatio,
scaleX: 1./fabric.devicePixelRatio,
left: bound.left/fabric.devicePixelRatio,
top:bound.top/fabric.devicePixelRatio
});
this.canvas.clearContext(ctx);
canvas.add(img);
}
setTimeout(trimbrushandcopytocanvas, this._interval); // added delay because last spray was on delay and may not have finished
},
The setTimeout function was used because Fabric Brush could still be drawing to the upper canvas after the mouseup event occurred, and there were occasions where the brush would continue painting the upper canvas after its context was cleared.
I am working on ionic based application.
I want to functionality like user has Filled the red color on image(canvas) with finger. So I have done the filled functionality but I want to crop the Filled portion from canvas. I have attached one image for reference.
I want to crop red portion from above image. I has googling but not found any solution.
Creating a image mask.
If you are rendering the selection area (red) then the solution is simple.
Create a second canvas the same size as the drawing, and don't add it to the DOM. Draw the red marking content onto that canvas
The on the display canvas render the image first and then render that marking
canvas over the image with composite mode like "overlay" so that the original image can be seen and the marked areas are red.
Now you have two layers, one is the image and the other the mask you can use to get a copy of the marked content.
To do that create a 3rd canvas, draw the original image onto it, then set the composite mode to "destination-in". Then draw the mask over it. Only the marked pixels will remain.
See the example for more details
setTimeout(example,0); // ensures that the run us after parsing
function example(){
const ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var selectLayer = CImageCtx(w,h); // creates a canvas
var selectedContent = CImageCtx(w,h); // the selected content
document.body.appendChild(selectedContent);
var image = new Image; // the image
image.src = " https://i.stack.imgur.com/QhFct.png";
// updates the masked result
function updateSelected(){
var ctx = selectedContent.ctx;
ctx.drawImage(image,0,0);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(selectLayer,0,0);
ctx.globalCompositeOperation = "source-over";
}
function update(){
// if mouse down then
if(mouse.but){
// clear the mask if on the right image
if(mouse.oldBut === false && mouse.x > 256){
selectLayer.ctx.clearRect(0,0,w,h);
mouse.but = false;
}else{
// draw the red
selectLayer.ctx.fillStyle = "red";
fillCircle(mouse.x, mouse.y, 20, selectLayer.ctx);
}
// update the masked result
updateSelected();
}
// clear the canvas
ctx.clearRect(0,0,w,h);
// draw the image
ctx.drawImage(image,0,0);
// then draw the marking layer over it with comp overlay
ctx.globalCompositeOperation = "overlay";
ctx.drawImage(selectLayer,0,0);
ctx.globalCompositeOperation = "source-over";
mouse.oldBut = mouse.but;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
//#############################################################################
// helper functions not part of the answer
//#############################################################################
const mouse = {
x : 0, y : 0, but : false,
events(e){
const m = mouse;
const bounds = canvas.getBoundingClientRect();
m.x = e.pageX - bounds.left - scrollX;
m.y = e.pageY - bounds.top - scrollY;
m.but = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.but;
}
};
(["down","up","move"]).forEach(name => document.addEventListener("mouse" + name,mouse.events));
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
const fillCircle = (l,y=ctx,r=ctx,c=ctx) =>{if(l.p1){c=y; r=leng(l);y=l.p1.y;l=l.p1.x }else if(l.x){c=r;r=y;y=l.y;l=l.x}c.beginPath(); c.arc(l,y,r,0,Math.PI*2); c.fill()}
body { font-family : arial; }
canvas { border : 2px solid black; }
Draw on image and the selected parts are shown on the right<br>
Click right image to reset selection<br>
<canvas id="canvas" width=256 height=256></canvas>
Already masked.
If the red mask is already applied to the image then there is not much you can do apart from do a threshold filter depending on how red the image is. But even then you are going to have problems with darker areas, and areas that already contain red.
Unless you have the original image you will have poor results.
If you have the original image then you will have to access the image data and create a new image as a mask by comparing each pixel and selecting only pixels that are different. That will force you to same domain images only (or with CORS cross origin headers)
I have a set of D3js rectangles with a foreign object sitting in front of it, the example can be found here. In this example when you click on the header (Test N) a div is shown and the box around is increased/decreased in size to match the change.
Everything works fine until I zoom in or out. Then all of the sudden the sizing goes crazy. I think the code in question is this area...
function resize() {
$scope.nodeElements.each(function(d, i) {
console.log("Test");
var elemental = d3.select(this);
var rect = elemental.select("rect");
var fo = elemental.select("foreignObject");
var div = fo.select("div");
var client = div[0][0].getBoundingClientRect();
fo.attr("height", client.height)
fo.attr("width", client.width)
fo.attr("y", 100 * i);
rect.attr("fill", "green")
rect.attr("width", fo.attr("width"));
rect.attr("height", fo.attr("height"));
rect.attr("y", 100 * i);
})
}
So how do I allow for zoom while still re-sizing the rectangle to the proper height/width
You've not taken the zoomed scale into account when you set the width and height of your rect elements:
function resize() {
// get current scale of container
var scale = d3.transform($scope.container.attr("transform")).scale;
$scope.nodeElements.each(function(d, i) {
console.log("Test");
var elemental = d3.select(this);
var rect = elemental.select("rect");
var fo = elemental.select("foreignObject");
var div = fo.select("div");
var client = div[0][0].getBoundingClientRect();
fo.attr("height", client.height)
fo.attr("width", client.width)
fo.attr("y", 100 * i);
rect.attr("fill", "green");
rect.attr("width", (+fo.attr("width") * 1/scale[0])); //<-- scale width by it
rect.attr("height", (+fo.attr("height") * 1/scale[0])); //<-- scale height by it
rect.attr("y", 100 * i);
})
}
}
Updated plunker.
I have a fabricjs canvas that I need to be able to zoom in and out and also change the image/object inside several times.
For this I setup the canvas in the first time the page loads like this:
fabric.Object.prototype.hasBorders = false;
fabric.Object.prototype.hasControls = false;
canvas = new fabric.Canvas('my_canvas', {renderOnAddRemove: false, stateful: false});
canvas.defaultCursor = "pointer";
canvas.backgroundImageStretch = false;
canvas.selection = false;
canvas.clear();
var image = document.getElementById('my_image');
if (image != null) {
imageSrc = image.src;
if(imageSrc.length > 0){
fabric.Image.fromURL(imageSrc, function(img) {
img = scaleImage(canvas, img); //shrinks the image to fit the canvas
img.selectable = false;
canvas.centerObject(img);
canvas.setActiveObject(img);
canvas.add(img);
});
}
}
canvas.deactivateAll().renderAll();
Then when I need to change the image/object in the canvas or when the page reloads, I try to reset the canvas like this:
canvas.clear();
canvas.remove(canvas.getActiveObject());
var image = document.getElementById('my_image');
if (image != null) {
imageSrc = image.src;
if(imageSrc.length > 0){
fabric.Image.fromURL(imageSrc, function(img) {
img = scaleImage(canvas, img); //shrinks the image to fit the canvas
img.selectable = false;
canvas.centerObject(img);
canvas.setActiveObject(img);
canvas.add(img);
});
}
}
Not sure if it matters but the way I change the image is by changing the source in 'my_image' and reseting the canvas with the above method.
This works well until I use canvas.zoomToPoint, as per this thread, after this, the image/object starts changing position when I reset the zoom or click the canvas with the mouse while it is zoomed, seeming to jump at each change in the top left corner direction, eventually disappearing from view.
Reset Zoom:
canvas.setZoom(1);
resetCanvas(); //(above method)
How can I restore the image/object position?
I tried doing the initial setup instead of the reset and seamed to work visually but was in fact adding a new layer of upper canvas at each new setup so it is no good.
Is there a way to reset the canvas to original state without causing this behavior and still be able to zoom in/out correctly?
Although this question is very old, here is what I did using the current version of fabric.js 2.2.4:
canvas.setViewportTransform([1,0,0,1,0,0]);
For your information: zooming to a point is a recalculation of the viewport transformation. The upper matrix is this is the initial viewport transform matrix.
I eventually fixed the problems I was having.
To reset the zoom, instead of just setting the zoom to 1 with canvas.setZoom(1), I reapplied the canvas.zoomToPoint method to the same point but with zoom 1, to force the initial zoom but regarding the same point that was used to zoom in.
As for the problem of restoring the image position in canvas (after panning for instance) it is as simple as removing the image, centering it in the canvas and re-adding it to the canvas as was done when adding first time:
var img = canvas.getActiveObject();
canvas.remove(img);
canvas.centerObject(img);
canvas.setActiveObject(img);
canvas.add(img);
canvas.renderAll();
See below snippet - here I do the same - zooming together, but degrouping the objects in case somebody clicks on it.
The problem to get to original object properties can be solved, ungrouping the group and creating copies of them and reattaching - a bit annoying, but the only solution I found.
<script id="main">
// canvas and office background
var mainGroup;
var canvas = this.__canvas = new fabric.Canvas('c');
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
createOnjects(canvas);
// events - zoom
$(canvas.wrapperEl).on('mousewheel', function(e) {
var target = canvas.findTarget(e);
var delta = e.originalEvent.wheelDelta / 5000;
if (target) {
target.scaleX += delta;
target.scaleY += delta;
// constrain
if (target.scaleX < 0.1) {
target.scaleX = 0.1;
target.scaleY = 0.1;
}
// constrain
if (target.scaleX > 10) {
target.scaleX = 10;
target.scaleY = 10;
}
target.setCoords();
canvas.renderAll();
return false;
}
});
// mouse down
canvas.on('mouse:up', function(options) {
if (options.target) {
var thisTarget = options.target;
var mousePos = canvas.getPointer(options.e);
if (thisTarget.isType('group')) {
// unGroup
console.log(mousePos);
var clone = thisTarget._objects.slice(0);
thisTarget._restoreObjectsState();
for (var i = 0; i < thisTarget._objects.length; i++) {
var o = thisTarget._objects[i];
if (o._element.alt == "officeFloor")
continue;
else {
if (mousePos.x >= o.originalLeft - o.currentWidth / 2 && mousePos.x <= o.originalLeft + o.currentWidth / 2
&& mousePos.y >= o.originalTop - o.currentHeight / 2 && mousePos.y <= o.originalTop + o.currentHeight / 2)
console.log(o._element.alt);
}
}
// remove all objects and re-render
canvas.remove(thisTarget);
canvas.clear().renderAll();
var group = new fabric.Group();
for (var i = 0; i < clone.length; i++) {
group.addWithUpdate(clone[i]);
}
canvas.add(group);
canvas.renderAll();
}
}
});
// functions
function createOnjects(canvas) {
// ToDo: jQuery.parseJSON() for config file (or web service)
fabric.Image.fromURL('pics/OfficeFloor.jpg', function(img) {
var back = img.set({ left: 100, top: 100 });
back._element.alt = "officeFloor";
back.hasControls = false;
fabric.Image.fromURL('pics/me.png', function(img) {
var me = img.set({ left: -420, top: 275 });
me._element.alt = "me";
console.log(me);
var group = new fabric.Group([ back, me], { left: 700, top: 400, hasControls: false });
canvas.clear().renderAll();
canvas.add(group);
// remove all objects and re-render
});
});
}
</script>
I have an image in a canvas. The image contains squares of different colors. I'd to click on a square and get the dimensions of the square. (pixels)
For example click on a red square with yellow border and return 24 X 100
I have seen code like this . which I think is part of the solution.... but can't seem to get it work.
Any ideas?
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
// Let's draw a green square
ctx.fillStyle = "rgb(0,127,0)";
ctx.fillRect(10, 10, 20, 20);
// We will now get two pixels, the first one filling inside the above square and the other outside the square
var Pixel = ctx.getImageData(29, 10, 2, 1);
// Let's print out the colour values of the first pixel. Since we have set the colour of the
// square as rgb(0,127,0), that is what the alert should print out. Since we have not set
// any alpha value, Pixel.data[3] should be the defualt 255.
alert("Pixel 1: " + Pixel.data[0] + ", " + Pixel.data[1] + ", " + Pixel.data[2] + ", " + Pixel.data[3]);
// Print out the second pixel, which is outside the square.
// since we have drawn nothing there yet, it will show the default values 0,0,0,0 (Transparent Black)
alert("Pixel 2: " + Pixel.data[4] + ", " + Pixel.data[5] + ", " + Pixel.data[6] + ", " + Pixel.data[7]);
// Let's get the width and height data from Pixel
alert("Pixels Width: " + Pixel.width);
alert("Pixels Height: " + Pixel.height);
}
}
You could use .getImageData, but there's a much easier way...
Save the information about each square you're drawing in an object:
var green={x:10,y:10,width:20,height:20,color:"rgb(0,127,0)"};
Put all objects in an array:
var squares=[];
squares.push(green);
Then when the user clicks, enumerate all the saved squares and see if the mouse is over one:
for(var i=0;i<squares.length;i++){
var s=squares[i];
if(mouseX>=s.x && mouseX<=s.x+s.width && mouseY>=s.y && mouseY<=s.y+s.height){
alert("You clicked on a square with size: "+s.width+"x"+s.height);
}
}
[ Addition based on questioner's comment (that comment was deleted by questioner) ]
Questioner's deleted comment:
What if I don't have info on the squares. The squares are from a photo?
Assuming the canvas contains colored rectangles, here's how to calculate the width x height of the colored rectangle containing the specified x,y coordinate:
function calcColorBounds(x,y){
var data=ctx.getImageData(0,0,canvas.width,canvas.height).data;
var n=i=(y*canvas.width+x)*4;
var r=data[i];
var g=data[i+1];
var b=data[i+2];
var minX=x;
while(minX>=0 && data[i]==r && data[i+1]==g && data[i+2]==b){
minX--;
i=(y*canvas.width+minX)*4;
}
i=n;
var maxX=x;
while(maxX<=canvas.width && data[i]==r && data[i+1]==g && data[i+2]==b){
maxX++;
i=(y*canvas.width+maxX)*4;
}
i=n;
var minY=y;
while(minY>=0 && data[i]==r && data[i+1]==g && data[i+2]==b){
minY--;
i=(minY*canvas.width+x)*4;
}
i=n;
var maxY=y;
while(maxY<=canvas.height && data[i]==r && data[i+1]==g && data[i+2]==b){
maxY++;
i=(maxY*canvas.width+x)*4;
}
alert("size",maxX-minX-1,"x",maxY-minY-1);
}