skip to Main Content

I am trying to create a curve with a gradient that follows it tangentially. Since its not possible to do that by default, i’ve taken to splitting the curve into a series of quadrilaterals and applying a gradient to each one. However, it seems that the gradient is not going in the correct direction. I’ve made the quadrilaterals larger to make it easier to see here. The black lines represent the x0, y0 and x1, y1 inputs to ctx.createLinearGradient() for each shape.

So i calculate the x and y for points 1, 2, 3, 4 (which go in clockwise direction around the shape starting from the top left), and then calculate the gradient direction line from the middle of the top of the shape to the middle of the bottom.

ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);

const grd_x1 = (x1 + x2) / 2;
const grd_y1 = (y1 + y2) / 2;
const grd_x2 = (x3 + x4) / 2;
const grd_y2 = (y3 + y4) / 2;

const gradient = ctx.createLinearGradient(
    grd_x1,
    grd_y1,
    grd_x2,
    grd_y2
);
gradient.addColorStop(0, "#3acbfcc3");
gradient.addColorStop(1, "#ffffff00");
ctx.fillStyle = gradient;
ctx.fill();

Hence i would expect the gradient would follow the direction of the black lines like this. However, in the shapes it is clear that the gradient is not going in that direction like this. I expect that the top two corner have the same colour (blue) and the bottom two corners have the same colour (white). Am i interpreting the inputs to the function wrong? The diagram at the top of this doc makes me think it should work.

2

Answers


  1. This doesn’t work with gradients…

    ctx.fill();
    

    So replace it with this…

    ctx.fillRect(0,0,C.width,C.height);
    

    Or specify some other region… but it’s gotta be a region.

    NB: Change "C" with your canvas identifier.


    This test is for somethinghere

    JS:

    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    
    let gradient = ctx.createLinearGradient(0, 0, 200, 0);
    
    gradient.addColorStop(0, "green");
    gradient.addColorStop(1, "pink");
    
    ctx.fillStyle = gradient;
    
    ctx.fill();  // <-- here's your idea
    
    //ctx.fillRect(10, 10, 200, 100); // <-- here's mine
    

    HTML:

    <canvas id="canvas"></canvas>
    

    Which one works?

    Login or Signup to reply.
  2. However paradoxical it may seem at first glance, but in your case gradients shouldn’t be rendered the way how you intuitively think you want them to be oriented.

    As can be seen from the article you already linked to:

    Gradients are rendered parallel to the specified vector. Which means that the color is constant along any perpendicular to that vector.


    Yet your vectors are not orthogonal to the frontal segments of the wave, so you end up with gradients which are:

    • Changing their colors along frontal segments, where color is expected to be constant.
    • Misaligned with each other, while they should be visually continuous.



    In order to align gradients properly, you have to make them orthogonal to their corresponding frontal segments, and in the same time ensure that colors propagate uniformly along common desired gradient vectors.

    How both requirements are met, can be visualized using a parallelogram created from the frontal segment and desired gradient vector attached to it (which is how you are already constructing your quadrilaterals). Now you only have to connect the line of the frontal side with the line of the opposite side, by drawing a perpendicular.

    This perpendicular is the solution you are looking for.

    The starting point can lie anywhere on the frontal segment, since by definition (and by design) it will be orthogonal to the gradient. The ending point can be found by solving linear algebra equations.


    I wanted to practice anyway, so here’s the demo!

    function renderWaveSegment ({ ctx, x1, y1, x2, y2, xGradient, yGradient, color1, color2 }) {
    
        // Linear algebra to calc proper ending point of the gradient
        let v1x = y2 - y1;
        let v1y = x1 - x2;
        let v3x = x2 - x1;
        let v3y = y2 - y1;
        let t = (v3x * yGradient - v3y * xGradient) / (v1y * v3x - v1x * v3y);
        let xg = x1 + v1x * t;
        let yg = y1 + v1y * t;
    
        let gradient;
        try {
            // Just in case if there're NaNs or Infinities and createLinearGradient decides to throw,
            // don't let it ruin the party!
            gradient = ctx.createLinearGradient(x1, y1, xg, yg);
        }
        catch (e) { return; }
        gradient.addColorStop(0, color1);
        gradient.addColorStop(1, color2);
    
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.lineTo(x2 + xGradient, y2 + yGradient);
        ctx.lineTo(x1 + xGradient, y1 + yGradient);
        ctx.closePath();
        ctx.fillStyle = gradient;
        ctx.fill();
    }
    
    function renderWave ({ ctx, xStart, yStart, xEnd, yEnd, step, phase, period, amplitude, xGradient, yGradient, color1, color2 }) {
    
        let length = Math.hypot(xEnd - xStart, yEnd - yStart);
        let xUnit = (xEnd - xStart) / length;
        let yUnit = (yEnd - yStart) / length;
        let xNormal = -yUnit * amplitude;
        let yNormal =  xUnit * amplitude;
        period /= Math.PI * 2;
    
        function waveFrontAt (pos) {
            let sin = Math.sin(phase + pos / period);
            return [
                xStart + pos * xUnit + sin * xNormal,
                yStart + pos * yUnit + sin * yNormal
            ];
        }
    
        // Reduce aliasing artifacts between segments by slightly overlapping them
        let overlap = 0.0525;
    
        for (var pos = 0; pos < length - step; pos += step) {
            let [x1, y1] = waveFrontAt(pos - overlap);
            let [x2, y2] = waveFrontAt(pos + step + overlap);
            renderWaveSegment({ ctx, x1, y1, x2, y2, xGradient, yGradient, color1, color2 });
        }
    
        let [x1, y1] = waveFrontAt(pos - overlap);
        let [x2, y2] = waveFrontAt(length + overlap);
        renderWaveSegment({ ctx, x1, y1, x2, y2, xGradient, yGradient, color1, color2 });
    }
    
    let canvas = document.getElementById('canvas');
    let ctx = canvas.getContext('2d');
    
    function render () {
    
        let time = Date.now() / 1000;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        renderWave({
            ctx,
    
            // Starting point of the wave
            xStart: -50,
            yStart: canvas.height * (Math.sin(time * 1.77) * 0.3 + 0.4),
    
            // Ending point of the wave
            xEnd: canvas.width + 50,
            yEnd: canvas.height * (Math.sin(time * 1.22) * 0.3 + 0.4),
    
            // Width of segment in pixels (along the baseline)
            step: 5,
    
            // Sine function (phase in radians, period and amplitude in pixels)
            phase: time * 10,
            period: Math.sin(time * 0.77) * 30 + 70,
            amplitude: 10,
    
            // Desired gradient vector and colors
            xGradient: Math.sin(time * 2.11) * 40,
            yGradient: Math.sin(time * 3.33) * 35 + 85,
            color1: '#3acbfcc3',
            color2: '#ffffff00'
        });
        window.requestAnimationFrame(render);
    }
    
    render();
    <canvas id="canvas" width="500" height="175"></canvas>

    Also note that iteratively filling paths with CanvasRenderingContext2D always leaves gaps between adjacent shapes, which otherwise are expected to be rendered seamlessly. This is an unfortunate "feature" of antialiased rendering when performed in several steps on top of each other rather than drawing everything at once (but canvas is leaving us with no choice anyway). I tried to minimize these artifacts by slightly overlapping quadrilaterals, so I hope they are not so noticeable and annoying now.

    Of course, there’s WebGL at everyone’s service, which allows to make quality and performant shaders, provided that one is ready to take the time to learn it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search