CSS linear-gradient and Canvas linear-gradient aren't the same with opacity settings

我与影子孤独终老i 提交于 2021-01-02 20:51:34

问题


I want to achieve the same linear gradient look defined by CSS on a canvas. Used a method that works great until no transparency setting is used. When there are rgba color values defined with the same linear gradient color settings the results doesn't look the same, please see the following link:

JSFiddle: Example

var canvas = document.getElementById("myCanvas");
var ctx = document.getElementById("myCanvas").getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cssAng = Math.PI;
var dir = getDir(cssAng, w, h);
var gr = ctx.createLinearGradient(dir.x0,dir.y0,dir.x1,dir.y1);
gr.addColorStop(0, "rgb(255, 255, 255, 0)");
gr.addColorStop(0.87, "rgb(0, 0, 0, 1)");
ctx.fillStyle = gr;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);


function getDir(radian, width, height) {
        radian += Math.PI;
        const HALF_WIDTH = width * 0.5;
    const HALF_HEIGHT = height * 0.5;
    const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
    const HALF_LINE_LENGTH = lineLength / 2;

    const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH;
    const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH;
    const x1 = width - x0;
    const y1 = height - y0;

return {x0, x1, y0, y1};
}
<!DOCTYPE html>
<html>
<body>
<div style='background-color:gray;display:inline-block;max-height:300px'>
  <div id="myDiv" style="display:inline-block;width:300px;height:300px;border:1px solid #d3d3d3;background:linear-gradient(180deg,rgba(255,255,255, 0) 0%, rgba(0,0,0,1) 87%"> </div>
</div>
<canvas id="myCanvas" width="300" height="300" style="background-color: gray;border:1px solid #d3d3d3;"> </canvas>
</body>
</html>

Any idea why is this happening? Is there a package that can handle this issue?


回答1:


There's actually a difference in the specs of the CSS linear-gradient and canvas linear gradient. They look almost exactly the same, except for the way the color needs to be calculated regarding the alpha value. For the CSS linear-gradient, you have this:

3.4.2. Coloring the Gradient Line At each color stop position, the gradient line is the color of the color stop. Before the first color stop, the gradient line is the color of the first color stop, and after the last color stop, the gradient line is the color of the last color stop. Between two color stops, the gradient line’s color is interpolated between the colors of the two-color stops, with the interpolation taking place in premultiplied RGBA space.

See: https://drafts.csswg.org/css-images-3/#coloring-gradient-line

Whereas the canvas one:

Once a gradient has been created (see below), stops are placed along it to define how the colors are distributed along the gradient. The color of the gradient at each stop is the color specified for that stop. Between each such stop, the colors and the alpha component must be linearly interpolated over the RGBA space without pre multiplying the alpha value to find the color to use at that offset. Before the first stop, the color must be the color of the first stop.

See: https://html.spec.whatwg.org/multipage/canvas.html#interpolation

So the CSS version calculates the color stops by premultiplying their alpha values. I've changed your example to make it a bit more obvious. In the example below, the CSS version goes from rgba(255, 0, 0, 0) or rgba(0, 0, 0, 0) to rgba(0, 0, 0, 1). So at 50% the color calculated using premultiplied alpha is rgba(0, 0, 0, 0.5).

In the canvas version, the interpolation is calculated without pre multiplying. So at 50% you have rgba(127,5, 0, 0, 0.5). This is true for every point of the gradient-line.

See What does premultiplied means: https://drafts.csswg.org/css-images-3/#premultiplied

And the example:

var canvas = document.getElementById("myCanvas");
var ctx = document.getElementById("myCanvas").getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cssAng = Math.PI;
var dir = getDir(cssAng, w, h);
var gr = ctx.createLinearGradient(dir.x0,dir.y0,dir.x1,dir.y1);
gr.addColorStop(0, "rgb(255, 0, 0, 0)");
gr.addColorStop(1, "rgb(0, 0, 0, 1)");
ctx.fillStyle = gr;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);


function getDir(radian, width, height) {
        radian += Math.PI;
        const HALF_WIDTH = width * 0.5;
    const HALF_HEIGHT = height * 0.5;
    const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
    const HALF_LINE_LENGTH = lineLength / 2;

    const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH;
    const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH;
    const x1 = width - x0;
    const y1 = height - y0;

return {x0, x1, y0, y1};
}
<!DOCTYPE html>
<html>
<body>
<div style='display:inline-block;max-height:300px'>
  <div id="myDiv" style="display:inline-block;width:200px;height:300px;border:1px ;background:linear-gradient(180deg,rgba(255,0,0, 0) 0%, rgba(0,0,0,1) 100%"> </div>
   
</div>
<canvas id="myCanvas" width="200" height="300" > </canvas>
<div style="position: absolute;width:8px;height:8px;background:rgba(0,0,0,0.5); top: 144px; left: 0px; ">

</div>
<div style="position: absolute;width:10px;height:10px;background:rgba(127.5,0,0,0.5); top: 144px; left: 412px; ">

</div>
</body>
</html>

I don't think there's a way to make the 2 equivalent, except by calculating every point of the gradient line.




回答2:


Can't say if it's the correct fix for your issue, but it's a workaround. Add a layer in between and proportionate the black, gray and white part. They're totally different, add more colors stops to get more control of the colors placement I guess.

var canvas = document.getElementById("myCanvas");
var ctx = document.getElementById("myCanvas").getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cssAng = Math.PI;
var dir = getDir(cssAng, w, h);
var gr = ctx.createLinearGradient(dir.x0,dir.y0,dir.x1,dir.y1);
gr.addColorStop(0, "rgb(255,255,255)");
gr.addColorStop(0, "rgb(125,125,125)");
gr.addColorStop(.9, "rgb(0,0,0)" );
ctx.fillStyle = gr;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);


function getDir(radian, width, height) {
        radian += Math.PI;
        const HALF_WIDTH = width * 0.5;
    const HALF_HEIGHT = height * 0.5;
    const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
    const HALF_LINE_LENGTH = lineLength / 2;

    const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH;
    const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH;
    const x1 = width - x0;
    const y1 = height - y0;

return {x0, x1, y0, y1};
}
<!DOCTYPE html>
<html>
<body>
<div style='background-color:gray;display:inline-block;max-height:300px'>
  <div id="myDiv" style="display:inline-block;width:300px;height:300px;border:1px solid #d3d3d3;background:linear-gradient(180deg,rgba(255,255,255, 0) 0%, rgba(0,0,0,1) 87%"> </div>
</div>
<canvas id="myCanvas" width="300" height="300" style="background-size: cover;background-color: gray;border:1px solid #d3d3d3;"> </canvas>
</body>
</html>


来源:https://stackoverflow.com/questions/62993447/css-linear-gradient-and-canvas-linear-gradient-arent-the-same-with-opacity-sett

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!