I want to free-draw shapes with fabric.js. The outline is, say, 20px (such that the user sees it clearly).
After the user has drawn it, the shape should been filled with the same color as the outline.
The whole thing should be semi-transparent. Unfortunately, this causes the overlap between outline and fill to be less transparent and draws a strange "inner outline" to the shape.
Is there a way to make shape uniquely semi-transparent?
Maybe a trick would be: after the user has drawn the shape, "widen" the shape by half of outline thickness and set outline thickness to 1. Would that be possible?
See this https://jsfiddle.net/4ypdwe9o/ or below for an example.
var canvas = new fabric.Canvas('c', {
isDrawingMode: true,
});
canvas.freeDrawingBrush.width = 10;
canvas.freeDrawingBrush.color = 'rgb(255, 0, 0)';
canvas.on('mouse:up', function() {
canvas.getObjects().forEach(o => {
o.fill = 'rgb(255, 0, 0)';
o.opacity = 0.5;
});
canvas.renderAll();
})
canvas {
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.js"></script>
<canvas id="c" width="600" height="600"></canvas>
It's a bit tricky but can be solved using a temporary canvas. So you first render the path using a solid color fill on the temporary canvas, then copy it to the main canvas like this:
//create temporary canvas
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = canvas.width;
tmpCanvas.height = canvas.height;
var tmpCtx = tmpCanvas.getContext("2d");
//remember the original render function
var pathRender = fabric.Path.prototype.render;
//override the Path render function
fabric.util.object.extend(fabric.Path.prototype, {
render: function(ctx, noTransform) {
var opacity = this.opacity;
//render the path with solid fill on the temp canvas
this.opacity = 1;
tmpCtx.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
pathRender.apply(this, [tmpCtx]);
this.opacity = opacity;
//copy the path from the temp canvas
ctx.globalAlpha = opacity;
ctx.drawImage(tmpCanvas, 0, 0);
}
});
See plunker here: https://plnkr.co/edit/r1Gs2wIoWSB0nSS32SrL?p=preview
In fabric.js 1.7.3, they have another implementation. When I use
fabricCanvasObject.getObjects('path').slice(-1)[0].setFill("red");
fabricCanvasObject.getObjects('path').slice(-1)[0].setOpacity(0.5);
instead of
fabricCanvasObject.getObjects('path').slice(-1)[0].fill = "red";
fabricCanvasObject.getObjects('path').slice(-1)[0].opacity = 0.5;
The boundary is painted correctly, without overlap. So, the temporary canvas from Janusz's answer is not needed anymore. For my former fabric.js 1.5.0, the answer from Janusz solved the problem.
Jetic,
You are almost finished your logic. Instead of using "opacity" use rgba:
canvas.getObjects().forEach(o => {
o.fill = 'rgba(255, 0, 0, 0.5)';
o.stroke = 'rgba(255, 0, 0, 0)';
// o.opacity = 0.5;
});
canvas.renderAll();
Related
I'm trying to create an app that lets you create a background gradient with two different colors,I'm using React. The first color of the gradient looks as it should, but the second color of the gradient is more of a solid color, with a jagged border. Heres a picture to demonstrate:
My goal is to get something that closer resembles to this:
Where the colors blend.
I'm referring to the MDN docs , and have messed around with the radius and x, y properties. I'm changing the canvas based on prop changes from the parent component, here's my code:
import React, { Component } from "react";
class Canvas extends Component {
componentDidMount() {
const { gradientOne, gradientTwo } = this.props.canvasState.backgroundColor;
this.ctx = this.canvas.getContext("2d");
this.radialGradient = this.ctx.createRadialGradient(
0,
0,
300,
260,
160,
100
);
this.ctx.fillStyle = this.radialGradient;
this.ctx.rect(0, 0, this.canvas.width, this.canvas.height);
this.radialGradient.addColorStop(0, gradientOne);
this.radialGradient.addColorStop(1, gradientTwo);
this.ctx.fill();
}
componentDidUpdate(prevProps, prevState) {
const { gradientOne, gradientTwo } = this.props.canvasState.backgroundColor;
if (prevProps.canvasState.backgroundColor.gradientOne !== gradientOne) {
this.ctx.fillStyle = this.radialGradient;
this.radialGradient.addColorStop(0, gradientOne);
this.ctx.fill();
} else if (
prevProps.canvasState.backgroundColor.gradientTwo !== gradientTwo
) {
this.ctx.fillStyle = this.radialGradient;
this.radialGradient.addColorStop(1, gradientTwo);
this.ctx.fill();
}
}
render() {
return (
<main className="canvasContainer">
<canvas ref={ref => (this.canvas = ref)} id="canvas">
YOUR BROWSER DOESN'T SUPPORT THIS FEATURE :(
</canvas>
</main>
);
}
}
export default Canvas;
Thanks for any help!
Color stops of CanvasGradient (be it linear or radial) can't be modified nor removed.
When you add a new color stop, at the same index than a previous one, it gets placed right after that previous one. So instead of having only two color stops, you have four.
This means that if you have an original gradient like so
<-red -------------------------------------------- green->
and that you add two new color stops blue and yellow at position 0 and 1, you will have something like
<-red[blue -------------------------------- green]yellow->
That is, no gradient between red and blue, nor between yellow and green:
const ctx = canvas.getContext('2d');
ctx.strokeStyle = 'white';
// an horizontal gradient
// 0 is at pixel 50, and 1 at pixel 250 on the x axis
const grad = ctx.createLinearGradient(50,0,250,0);
grad.addColorStop(0, 'red');
grad.addColorStop(1, 'green');
ctx.fillStyle = grad;
// top is two color stops version
ctx.fillRect(0,0,300,70);
// bottom is four color stops version
grad.addColorStop(0, 'blue');
grad.addColorStop(1, 'yellow');
ctx.fillStyle = grad;
ctx.fillRect(0,80,300,70);
// mark color stops
ctx.moveTo(49.5,0);
ctx.lineTo(49.5,150);
ctx.moveTo(249.5,0);
ctx.lineTo(249.5,150);
ctx.stroke();
canvas { border: 1px solid };
<canvas id="canvas"></canvas>
That's what you are doing in your code, since you add two color stops in componentDidMount, and then add more in componentDidUpdate.
To avoid this, simply overwrite your gradient property in componentDidUpdate, so that you start everytime with a new gradient.
const ctx = canvas.getContext('2d');
let radialGradient;
function didMount() {
radialGradient = ctx.createRadialGradient(
0,0,300,
260,160,100
);
radialGradient.addColorStop(0, c1.value);
radialGradient.addColorStop(1, c2.value);
ctx.fillStyle = radialGradient;
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function didUpdate() {
// reset radialGradient to a new one
radialGradient = ctx.createRadialGradient(
0,0,300,
260,160,100
);
radialGradient.addColorStop(0, c1.value);
radialGradient.addColorStop(1, c2.value);
ctx.fillStyle = radialGradient;
ctx.fillRect(0,0,canvas.width,canvas.height);
}
didMount();
c1.oninput = c2.oninput = didUpdate;
canvas { border: 1px solid };
<input id="c1" type="color" value="#22CC22">
<input id="c2" type="color" value="#FF2222">
<canvas id="canvas"></canvas>
This question already has answers here:
Canvas is stretched when using CSS but normal with "width" / "height" properties
(10 answers)
Closed 4 years ago.
I have a fairly simple test, a 400 x 400 white image with text on it saying "1" over and over again.
I draw it on a fairly simple 1000 x 1000 canvas, trying to resize it to 100 x 100.
var image = new Image();
document.body.appendChild(image);
image.addEventListener("load",function (event) {
var image1 = event.target;
var tempCanvas = window.document.createElement("canvas");
tempCanvas.style.width = "1000px";
tempCanvas.style.height = "1000px";
tempCanvas.style.background = "rgba(0, 0, 0, 0.1)";
document.body.appendChild(tempCanvas);
tempCanvas.getContext("2d").drawImage(image1, 0, 0, 100, 100);
});
image.src = "1.png";
But despite all that being squares, I end up with an odd-looking, deformed, weirdly scaled result that's rectangular, low quality, and without having any of its dimensions being 100px.
On the left, you can see the original image, on the right, that's the top left corner of my canvas.
If you want the original image, here it is: https://i.stack.imgur.com/yDkLx.png
What am I missing?
Try setting the width and height values on the canvas element prior to the drawImage(), rather than relying on the styles as you are.
Setting the width and height attributes corresponds to setting the dimensions of that canvas element.
Once you've defined the dimensions of a canvas element, the rendering behavior of the canvas becomes much more predicatable:
var image = new Image();
document.body.appendChild(image);
image.addEventListener("load",function (event) {
var image1 = event.target;
var tempCanvas = window.document.createElement("canvas");
//tempCanvas.style.width = "400px";
//tempCanvas.style.height = "400px";
tempCanvas.style.background = "rgba(0, 0, 0, 0.1)";
tempCanvas.width = 100;
tempCanvas.height = 100;
document.body.appendChild(tempCanvas);
tempCanvas.getContext("2d").drawImage(image1, 0, 0, 100, 100);
});
image.src = "https://puu.sh/C4HE2/d96b531d08.png";
canvas {
border:1px solid blue;
}
img {
border:1px solid red;
}
The code snippet above shows the original source image with red border, and the down-scaled canvas rendered image with blue border - hope this helps!
Creating an editing tool using fabricJS.. I want to add a reference axis to help user to align the object when the object is getting rotated. I need something like what is in the picture->
Basically I want to know how to modify the selection to add those axis
*I don't know why people downvote without bothering to even give a probable solution. The question I have doesn't require a snippet neither the solution is available elsewhere!
This is how you can modify a default fabricjs function mantaining the original behaviour and adding something on top of it.
var img02URL = 'http://fabricjs.com/lib/pug.jpg';
var originalDrawBorders = fabric.Object.prototype.drawBorders;
fabric.Object.prototype.drawBorders = function(ctx, styleOverride) {
originalDrawBorders.call(this, ctx, styleOverride);
var wh = this._calculateCurrentDimensions(),
strokeWidth = 1 / this.borderScaleFactor,
width = wh.x + strokeWidth + 40,
height = wh.y + strokeWidth + 40;
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(-width / 2, 0);
ctx.lineTo(width/ 2, 0);
ctx.moveTo(0, -height/2);
ctx.lineTo(0, height/2);
ctx.stroke();
}
var canvas = new fabric.Canvas('c');
canvas.setZoom(0.5)
fabric.Image.fromURL(img02URL, function(oImg) {
oImg.scale(.40);
oImg.left = 180;
oImg.top = 0;
canvas.add(oImg);
canvas.renderAll();
});
#c {
border:1px solid #ccc;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.17/fabric.min.js"></script>
<canvas id="c" width="400" height="400"></canvas>
This question already has an answer here:
HTML Canvas: Drawing grid below a plot
(1 answer)
Closed 6 years ago.
I have a canvas, and I want to use drawImage to draw an image behind the current content on the canvas.
Due to the fact that there is content already on the canvas (I'm using Literally Canvas to create a canvas containing an image, so I can't really draw the image first), I cannot use drawImage before I render the rest of my content.
Is it possible to drawImage behind all other content on a canvas?
Yes you can just use globalCompositeOperation destination-over, but note that your first image needs some transparency, otherwise, you will obviously not see anything :
var img1 = new Image();
var img2 = new Image();
var loaded = 0;
var imageLoad = function(){
if(++loaded == 2){
draw();
}
};
img1.onload = img2.onload = imageLoad;
var draw = function(){
var ctx = c.getContext('2d');
ctx.drawImage(img1, 100,100);
// wait a little bit before drawing the background image
setTimeout(function(){
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(img2, 0,0);
}, 500);
}
img1.src = "https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png";
img2.src = "https://picsum.photos/200/200";
<canvas id="c" width="200" height="200"></canvas>
Sorry about the previous post, I didn't properly read your post
Perhaps you could save the canvas, draw your image, and then reload the old content on top of your drawn image? Here's some JS psuedocode:
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
ctx.drawImage('Your Image Watermark Stuff');
ctx.putImageData(imgData,0,0);
You can use KonvaJS. And then use layers for it.
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.rawgit.com/konvajs/konva/0.13.0/konva.min.js"></script>
<meta charset="utf-8">
<title>Konva Rect Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #F0F0F0;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Konva.Layer();
var imageObj = new Image();
imageObj.onload = function() {
var baseImage = new Konva.Image({
x: 50,
y: 50,
width: width,
height: height,
image: image
});
// add the shape to the layer
layer.add(rect);
// add the layer to the stage
stage.add(layer);
};
imageObj.src = 'url to your image'
</script>
</body>
</html>
A simple solution would be to use another canvas behind the first one.
Normally canvas pixels are initialized to transparent black and therefore are perfectly see-through.
If your first canvas is created opaque instead the only other option I can think to is
create a temporary canvas of the same size
draw your image in this temporary canvas
get the ImageData object of both the temporary canvas and of the original canvas
copy from the temporary canvas to the original canvas only where the original canvas is not set at the background color
In code:
var tmpcanvas = document.createElement("canvas");
tmpcanvas.width = canvas.width;
tmpcanvas.height = canvas.height;
var temp_ctx = tmpcanvas.getContext("2d");
// ... draw your image into temporary context ...
var temp_idata = temp_ctx.getImageData(0, 0, canvas.width, canvas.height);
var temp_data = temp_idata.data;
// Access the original canvas pixels
var ctx = canvas.getContext("2d");
var idata = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = idata.data;
// Find the background color (here I'll use first top-left pixel)
var br_r = data[0], bg_g = data[1], bg_b = data[2];
// Replace all background pixels with pixels from temp image
for (var i=0,n=canvas.width*canvas.height*4; i<n; i+=4) {
if (data[i] == bg_r && data[i+1] == bg_g && data[i+2] == bg_b) {
data[i] = tmp_data[i];
data[i+1] = tmp_data[i+1];
data[i+2] = tmp_data[i+2];
data[i+3] = tmp_data[i+3];
}
}
// Update the canvas
ctx.putImageData(idata, 0, 0);
this approach however will have a lower quality if the original canvas graphics has been drawn with antialiasing or if pixels of the background color are also used in the image (e.g. an object on #FFF white background where object highlights are also #FFF). Another problem is if the background color is not a perfectly uniform RGB value (this will happen if the image has been compressed with a lossy algorithm like jpeg).
All these problems could be mitigated with more sophisticated algorithms like range matching, morphological adjustments and color-to-alpha conversions (basically the same machinery used for chroma-keying).
I currently have a canvas on which I draw 4 "shapes" which are displayed as an image.
The code for making the shapes:
function init() {
var s = new CanvasState(document.getElementById('canvas1'));
s.addShape(new Shape(40,40,180,180)); // The default is gray
s.addShape(new Shape(60,140,40,60, 'lightskyblue'));
// Some partially transparent
s.addShape(new Shape(80,150,60,30, 'rgba(127, 255, 212, .5)'));
s.addShape(new Shape(125,80,30,80, 'rgba(245, 222, 179, .7)'));
}
And drawing the shapes:
Shape.prototype.draw = function(ctx) {
var locx = this.x;
var locy = this.y;
var imgNew = new Image();
imgNew.onload = function(){
ctx.drawImage(imgNew, locx, locy);
}
imgNew.src = "upload/Evenredig.jpg";
}
As you can see I am currently setting the width and height of the shape manually but I would like to know if it is possible to get the width and height from the image and set that as the width and height of the shape.
The full code for the canvas can be found here (It may be helpfull??)
http://pastebin.com/Z3eqvVnw
ps. feel free to edit the title if you know a better one because I have no idea how to name this question.