I am trying to make a radial gradient with a Javascipt/HTML canvas. The problem is that the gradients don't overlap properly as if the alpha channel isn't working.
This is the code I am using:
var gradient1 = ctx.createRadialGradient(300, 300, 300, 300, 300, 0);
gradient1.addColorStop(0,"rgba(0, 0, 0, 0)");
gradient1.addColorStop(1,"#FF0000");
ctx.fillStyle = gradient1;
ctx.fillRect(x1, y1, 600, 600);
ctx.fillRect(x1, y1, 600, 600);
Here is a picture:
This for some reason fades to a black-like color rather than staying red. This leads it to act weird when two of these gradients of different colors are touching.
How can I make this react normally?
Cause
The gradient defined is red-black and both the color and the alpha channel will be interpolated . At 50% it will be halfway between red and black, but also 50% visible which is why it is becoming black-ish.
Cure
To fix make sure both color stops are the same color which just the alpha channel changed. This will keep the color the same all the way:
gradient1.addColorStop(0, "rgba(255, 0, 0, 0)"); // red, but transparent
gradient1.addColorStop(0, "#f00"); // red, solid
Click the link below to see this in action:
var ctx = document.querySelector("canvas").getContext("2d");
var gradient1 = ctx.createRadialGradient(300, 300, 300, 300, 300, 0);
gradient1.addColorStop(0,"rgba(255, 0, 0, 0)");
gradient1.addColorStop(1,"#FF0000");
ctx.fillStyle = gradient1;
ctx.fillRect(0, 0, 600, 600);
ctx.fillRect(0, 0, 600, 600);
<canvas width=600 height=600></canvas>
Related
I have a canvas and when i draw 2 things overlapping with low opacity, the opacity gets higher in the part where they are overlapping. Is there a way to make it all the same opacity even if 2 things are overlapping.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = "rgba(0, 0, 255, 0.2)";
ctx.fillRect(10, 10, 50, 50);
ctx.fillRect(20, 20, 50, 60);
ctx.fillRect(40, 5, 50, 40)
canvas {
width: 100vw;
height: 100vh;
}
<p>all of it should be the same color but the overlapping parts are darker</p>
<canvas id="canvas"></canvas>
For simple cases the usual trick is to use a second canvas as a layer: you draw the parts that shouldn't mix with full opacity on a detached canvas, and then you draw this canvas with the intended alpha on the visible one:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const detached = canvas.cloneNode();
const ctx2 = detached.getContext("2d");
// draw at full opacity
ctx2.fillStyle = "rgb(0, 0, 255)";
ctx2.fillRect(10, 10, 50, 50);
ctx2.fillRect(20, 20, 50, 60);
ctx2.fillRect(40, 5, 50, 40)
// draw something in the background of the visible canvas
// where we want the blue rects to mix with
ctx.fillStyle = "green";
ctx.fillRect(50, 65, 30, 30);
// now draw the blue rects in a single pass with the expected alpha
ctx.globalAlpha = 0.2
ctx.drawImage(detached, 0, 0);
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
Note that this exact example could have been done with a single canvas: if all your shape do share the same color, you can make them all be part of the same sub-path and fill them all in a single call.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(50, 65, 30, 30);
ctx.fillStyle = "rgb(0, 0, 255, 0.2)";
ctx.beginPath();
ctx.rect(10, 10, 50, 50);
ctx.rect(20, 20, 50, 60);
ctx.rect(40, 5, 50, 40);
ctx.fill();
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
Now these work only when the transparency is the same for all the shapes to be drawn. In case of multiple shapes with different alpha values, they'd still get mixed.
Here is an example of such a case.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(50, 10, 50, 60);
ctx.fillStyle = "rgba(0, 0, 255, .2)";
ctx.fillRect(40, 50, 70, 60);
ctx.fillStyle = "rgba(0, 0, 255, .8)";
ctx.fillRect(10, 20, 60, 70);
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
If we wanted the first blue rect to be over the second one without mixing, but still mix with the green one, we'd need to use a variation of the first solution, but use more steps:
First you calculate the alpha value of the more transparent rect relative to the more opaque one. Here we have 0.2 and 0.8, so if we want the 0.8 to be 1, 0.2 has to become 0.25.
We draw the more transparent rect at 0.25, then the more opaque one at 1 over this.
We redraw the composition with the target 0.8 alpha value.
We draw that over the green background.
However I'll take the opportunity of this new snippet to show that with some creative use of the globalCompositeOperation property we can make all this on a single canvas.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
// To use a single canvas, we will draw the green "background" later
// target color is rgb(0, 0, 255, 0.2) & max alpha is 0.8
// 0.2 x (1 / 0.8) -> 0.25
ctx.fillStyle = "rgb(0, 0, 255, 0.25)";
ctx.fillRect(40, 50, 70, 60);
ctx.fillStyle = "rgba(0, 0, 255, 1)";
ctx.fillRect(10, 20, 60, 70);
ctx.globalAlpha = 0.8;
// gCO "copy" will clear whatever was on the context before the next paint
ctx.globalCompositeOperation = "copy";
ctx.drawImage(ctx.canvas, 0, 0);
// we could continue over like this if more such opacities were required
ctx.globalAlpha = 1;
// gCO "destination-over" will draw behind what's already painted on the context
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = "green";
ctx.fillRect(50, 10, 50, 60);
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
Now, it may be of interest for some, but at WHATWG/html we started a discussion about enabling a layer API for the canvas2D, and while it's far from reaching a consensus, I wrote a not so little prototype of a CanvasLayer interface that would allow us to, well, use layers in a canvas.
The idea is to create a CanvasLayer object, that does record drawings operations that will get executed on it when it's rendered on a context, using the context's current settings. Basically reproducing the detached canvas trick, but with taking care automagically of the sizing of the detached canvas, and (if implemented natively), without assigning a full bitmap buffer.
In my very biased opinion (I'm the author of both that proposal and of the prototype), this would allow for clearer code when we have to deal with multiple layers over a canvas.
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
<script type="module">
import CanvasLayer from "https://cdn.jsdelivr.net/gh/Kaiido/CanvasLayer/bundles/CanvasLayer.min.mjs";
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(50, 10, 50, 60);
const backLayer = new CanvasLayer();
backLayer.fillStyle = "rgba(0, 0, 255)"; // fully opaque
backLayer.fillRect(40, 50, 70, 60);
const frontLayer = new CanvasLayer();
frontLayer.fillStyle = "rgba(0, 0, 255)";
frontLayer.fillRect(10, 20, 60, 70);
frontLayer.globalAlpha = 0.2;
frontLayer.renderLayer(backLayer);
ctx.globalAlpha = 0.8;
ctx.renderLayer(frontLayer);
</script>
I am trying to use canvas's globalCompositeOperation='destination-in' setting to draw a series of dots that are masked by a radial gradient. My desired outcome is shown in the screenshot below:
Instead, my canvas is showing the solid gradient with none of the dots visible. Here's my JS:
var canvas = document.getElementById('canvas')
, ctx = canvas.getContext('2d');
var coordMatrix = [
[50, 100, 150, 50, 100, 150],
[50, 50, 50, 100, 100, 100]
];
var gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 200);
ctx.globalCompositeOperation = 'destination-in';
coordMatrix[0].forEach(function(xCoord, i) {
var yCoord = coordMatrix[1][i];
ctx.moveTo(xCoord, yCoord);
ctx.arc(xCoord, yCoord, 10, 0, Math.PI * 2, false);
});
And here's a fiddle:
https://jsfiddle.net/73d9jawn/2/
Am I missing something?
You forgot to call ctx.fill() after setting the coordinates for the arcs. Also, you need to call ctx.fill() after the forEach has completed all iterations, otherwise globalCompositeOperation only applies to the first circle drawn. Here is an updated fiddle.
I'm making a game with javascript canvas. I'm drawing all the game elements, like the player, blocks and lines, but I don't want you to see all that. Instead want the whole screen to be black, expect for in some places where there is lightsources. I don't need any shadows, just a circle on the screen that is lit up with a radial gradient. I am able to achieve this for one lightsource by adding a transparent gradient after I have drawn everything else, like this: (imagine the red rectangle to be all the things in the game)
//Drawing all the game elements
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 400, 300);
//adding the darkness and the lightsources
var grd = ctx.createRadialGradient(150, 100, 5, 150, 100, 100);
grd.addColorStop(0, "transparent");
grd.addColorStop(1, "black");
ctx.fillStyle = grd; ctx.fillRect(0, 0, 600, 400);
JSFiddle
But how can I achieve this with multiple lightsources? The technique showed won't work.
I have tried using the Illuminated.js api, but that was incredibly slow, and I don't need anything of the shadows and all that fancy stuff, just a circle where you can see the game.
Here's an example of my approach - create black&white mask and multiply the base with it:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
//Drawing all the game elements
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 400, 300);
//adding the darkness and the lightsources
function addlight(ctx, x, y) {
var grd = ctx.createRadialGradient(x, y, 10, x, y, 150);
grd.addColorStop(0, "white");
grd.addColorStop(1, "transparent");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 600, 400);
}
var buffer = document.createElement('canvas');
buffer.width = 600;
buffer.height = 400;
b_ctx = buffer.getContext('2d');
b_ctx.fillStyle = "black";
b_ctx.fillRect(0, 0, 600, 400);
addlight(b_ctx, 150, 100);
addlight(b_ctx, 350, 200);
addlight(b_ctx, 450, 250);
ctx.globalCompositeOperation = "multiply";
ctx.drawImage(buffer, 0, 0);
<canvas id="myCanvas" width="600" height="400" style="border:1px solid #d3d3d3;">
I am currently coding an interactive application using Javascript and the HTML canvas element. One of the things I'm coding is an opacity gradient. My gradient is supposed to fade from transparent to partially opaque, but the entire area I defined as the place to create the gradient is only partially opaque with no fade from transparency. Here's my code:
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var grd = ctx.createLinearGradient(0, 1000, 0, 0);
grd.addColorStop(0.5, "rgba(255, 255, 255, 0.5)");
grd.addColorStop(1, "rgba(255, 255, 255, 0)");
ctx.fillStyle = grd;
ctx.fillRect(0, 660, 1000, 10);
}
It has something to with the dimensions you use in createLinearGradient and fillRect. Right now you are defining your gradient area as a really tall thin rectangle [(0,0) to (0,1000)]. However, then you draw your gradient rectangle as a pretty big rectangle [(0,660) to (1000,10)].
I'm not exactly sure what your desired output looks like but it seems that your ctx.fillRect line is wrong. Trying the following works for me:
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var grd = ctx.createLinearGradient(0, 1000, 0, 0);
grd.addColorStop(0.5, "rgba(255, 255, 255, 0.5)");
grd.addColorStop(1, "rgba(255, 255, 255, 0)");
ctx.fillStyle = grd;
ctx.fillRect(0, 10, 1000, 660);
}
And the jsFiddle if you are interested
I would like to obtain a radial gradient effect on an image (alpha = 1 in the middle and transparent on the edges).
Do you have any ideeas on how I could do that?
If I'm not mistaking what you want to do is:
Draw the image
Draw a radial gradient over it, where the borders are transparent and the middle is opaque and using the globalCompositeOperation setting on the context to blend the transparency gradient with the image.
You can rather easily translate this into code: http://jsfiddle.net/W8Ywp/1/.
var ctx = $('#cv').get(0).getContext('2d');
var img = new Image();
img.src = 'http://www.netstate.com/states/'
+ 'symb/flowers/images/oklahoma_rose.jpg';
img.onload = function() {
ctx.drawImage(img, 0, 0, 300, 300); // Draw image
// Create gradient, from middle to borders
var gradient = ctx.createRadialGradient(150, 150, 0,
150, 150, 150);
// Opaque white in the middle
gradient.addColorStop(0, 'rgba(255,255,255,0)');
// Transparent white at the borders
gradient.addColorStop(1, 'rgba(255,255,255,1)');
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 300, 300); // Fill rectangle over image with the gradient
};