Angle gradient in canvas
Asked Answered
G

4

10

I'm looking for a code that permits to have this effect on a canvas' stroke. I've already got an animated circular stroke, I only need to get the ANGLE gradient, not linear and not radial. I've got only 3 colours. The existing one is available here (the review rating)

Glaciate answered 6/3, 2014 at 12:3 Comment(3)
That's called a conical gradient.Dagmardagna
Probably you're right, but in Photoshop it's called "Angle" :)Glaciate
This post can also be helpful. #30186961Etymon
B
21

A context strokeStyle can be a gradient:

// create a gradient

gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd);
gradient.addColorStop(0.0,"blue");
gradient.addColorStop(1.0,"purple");


// stroke using that gradient

ctx.strokeStyle = gradient;

Example code and a Demo using a gradient strokeStyle: http://jsfiddle.net/m1erickson/w46ps/

enter image description here

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
    body{ background-color: ivory; }
    #canvas{border:1px solid red;}
</style>
<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    function drawMultiRadiantCircle(xc, yc, r, radientColors) {
        var partLength = (2 * Math.PI) / radientColors.length;
        var start = 0;
        var gradient = null;
        var startColor = null,
            endColor = null;

        for (var i = 0; i < radientColors.length; i++) {
            startColor = radientColors[i];
            endColor = radientColors[(i + 1) % radientColors.length];

            // x start / end of the next arc to draw
            var xStart = xc + Math.cos(start) * r;
            var xEnd = xc + Math.cos(start + partLength) * r;
            // y start / end of the next arc to draw
            var yStart = yc + Math.sin(start) * r;
            var yEnd = yc + Math.sin(start + partLength) * r;

            ctx.beginPath();

            gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd);
            gradient.addColorStop(0, startColor);
            gradient.addColorStop(1.0, endColor);

            ctx.strokeStyle = gradient;
            ctx.arc(xc, yc, r, start, start + partLength);
            ctx.lineWidth = 30;
            ctx.stroke();
            ctx.closePath();

            start += partLength;
        }
    }

    var someColors = [];
    someColors.push('#0F0');
    someColors.push('#0FF');
    someColors.push('#F00');
    someColors.push('#FF0');
    someColors.push('#F0F');

    drawMultiRadiantCircle(150, 150, 120, someColors);

}); // end $(function(){});
</script>
</head>
<body>
    <canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
Bobbinet answered 6/3, 2014 at 17:18 Comment(0)
M
6

Update April 2021

Someone created an npm package called create-conical-gradient which achieves exactly the same image, but much faster.

It adds a .createConicalGradient() method to CanvasRenderingContext2D.prototype. Its syntax is:

/**
 * @param ox The x-axis coordinate of the origin of the gradient pattern, which 
 *           default value is `0`.
 * @param oy The y-axis coordinate of the origin of the gradient pattern, which 
 *           default value is `0`.
 * @param startAngle The angle at which the arc starts in radians measured from 
 *                   the positive x-axis, which default value is `0`.
 * @param endAngle The angle at which the arc ends in radians measured from the 
 *                 positive x-axis, which default value is `2 * Math.PI`.
 * @param anticlockwise An optional `Boolean`. If `true`, draws the gradient 
 *                      counter-clockwise between the start and end angles. 
 *                      The default is `false` (clockwise).
 */
const gradient = ctx.createConicalGradient(ox, oy, startAngle, endAngle, anticlockwise);

Example

const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
const gradient = ctx.createConicalGradient(240, 135, -Math.PI, Math.PI);

gradient.addColorStop(0, '#f00');
gradient.addColorStop(0.2, '#00f');
gradient.addColorStop(0.4, '#0ff');
gradient.addColorStop(0.6, '#f0f');
gradient.addColorStop(0.8, '#ff0');
gradient.addColorStop(1, '#f00');

let isStroke = false;
const draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  ctx.arc(canvas.width / 2, canvas.height / 2, canvas.height / 2.5, 0, 2 * Math.PI);
  
  if (isStroke) {
    ctx.strokeStyle = gradient.pattern;
    ctx.lineWidth = 10;
    ctx.stroke();
  } else {
    ctx.fillStyle = gradient.pattern;
    ctx.fill();
  }
  
  ctx.closePath();
  isStroke = !isStroke;
};

draw();
canvas.addEventListener('click', draw);
<script src="https://unpkg.com/create-conical-gradient@latest/umd/create-conical-gradient.min.js"></script>
<canvas id="my-canvas" width="480" height="270">
  Your browser does not support canvas...
</canvas>

Original Answer

In my case, I needed the whole circle to be filled rather than just a stroke around the circumference. Using the answer above and setting the line width to twice the radius gave undesirable results, so I wrote my own.

/**
 * @description Options used when calling CanvasRenderingContext2D.strokeArcGradient() and 
 *              CanvasRenderingContext2D.fillArcGradient().
 * @property {Boolean} useDegrees Whether the specified angles should be interpreted as degrees rather than radians. 
 *                                (default: false)
 * @property {Number} resolutionFactor The number of lines to render per pixel along the arc.  A higher number produces 
 *                                     a cleaner gradient, but has worse performance for large radii.  Must be greater 
 *                                     than 0. (default: 8)
 */
class ArcGradientOptions {
    constructor(options) {
        function validateParam(test, errorMessage, fatal = false) {
            if (!test) {
                if (fatal) {
                    throw new Error(errorMessage);
                } else {
                    console.assert(false, errorMessage);
                }
            }
        }

        options = Object.assign({
            useDegrees: false,
            resolutionFactor: 8,
        }, options);

        validateParam(
            (options.resolutionFactor instanceof Number | typeof options.resolutionFactor === 'number') && 
                options.resolutionFactor > 0, 
            `ArcGradientOptions.resolutionFactor must be a Number greater than 0.  Given: ${options.resolutionFactor}`, 
            true);

        Object.assign(this, options);
    }
};

(function () {
    /**
     * @description Strokes an arc using a linear gradient.
     * @param {number} x The x-component of origin of the arc.
     * @param {number} y The y-component of the origin of the arc.
     * @param {number} radius The radius of the arc.
     * @param {number} startAngle Where in the circle to begin the stroke.
     * @param {number} endAngle Where in the circle to end the stroke.
     * @param {ArcGradientOptions} options Additional options.
     */
    CanvasRenderingContext2D.prototype.strokeArcGradient = function (x, y, radius, startAngle, endAngle, colorStops, 
            options) {
        options = new ArcGradientOptions(options);
        let lineWidth = this.lineWidth;
        this.fillArcGradient(x, y, startAngle, endAngle, colorStops, radius + lineWidth / 2, radius - lineWidth / 2, 
            options);
    }

    /**
     * @description Fills a sector or a portion of a ring with a linear gradient.
     * @param {number} x The x-component of origin of the arc
     * @param {number} y The y-component of the origin of the arc
     * @param {number} startAngle Where in the circle to begin the fill.
     * @param {number} endAngle Where in the circle to end the fill.
     * @param {number} outerRadius The radius of the arc.
     * @param {number} innerRadius The radius of the arc that won't be filled.  An innerRadius = 0 will fill the whole 
     *                             arc. (default: 0)
     * @param {ArcGradientOptions} options Additional options.
     */
    CanvasRenderingContext2D.prototype.fillArcGradient = function (x, y, startAngle, endAngle, colorStops, outerRadius, 
            innerRadius = 0, options) {
        options = new ArcGradientOptions(options);

        let oldLineWidth = this.lineWidth,
            oldStrokeStyle = this.strokeStyle;

        if (options.useDegrees) {
            startAngle = startAngle * Math.PI / 180;
            endAngle = endAngle * Math.PI / 180;
        }

        let deltaArcAngle = endAngle - startAngle;
            gradientWidth = Math.floor(outerRadius * Math.abs(deltaArcAngle) * options.resolutionFactor),
            gData = generateGradientImgData(gradientWidth, colorStops).data;

        this.lineWidth = Math.min(4 / options.resolutionFactor, 1);

        for (let i = 0; i < gradientWidth; i++) {
            let gradi = i * 4,
                theta = startAngle + deltaArcAngle * i / gradientWidth;

            this.strokeStyle = `rgba(${gData[gradi]}, ${gData[gradi + 1]}, ${gData[gradi + 2]}, ${gData[gradi + 3]})`;

            this.beginPath();
            this.moveTo(x + Math.cos(theta) * innerRadius, y + Math.sin(theta) * innerRadius);
            this.lineTo(x + Math.cos(theta) * outerRadius, y + Math.sin(theta) * outerRadius);
            this.stroke();
            this.closePath();
        }

        this.lineWidth = oldLineWidth;
        this.strokeStyle = oldStrokeStyle;
    }

    function generateGradientImgData(width, colorStops) {
        let canvas = document.createElement('canvas');
        canvas.setAttribute('width', width);
        canvas.setAttribute('height', 1);
        let ctx = canvas.getContext('2d'),
            gradient = ctx.createLinearGradient(0, 0, width, 0);
        
        for (let i = 0; i < colorStops.length; i++) {
            gradient.addColorStop(colorStops[i].offset, colorStops[i].color);
        }
        
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, width, 1);
        return ctx.getImageData(0, 0, width, 1);
    }
})();

This method draws lines from the center of the circle to each pixel along the edge of it. You get a cleaner gradient this way.

Circles side-by-side

For large line thicknesses, it's still cleaner.

Rings side-by-side

Its one major drawback is performance. If your radius is very large, the number of lines required to produce a nice circle is about 50 times the radius.

jsFiddle

Marplot answered 11/1, 2018 at 6:16 Comment(2)
Who asked for jquery? Only javascript is mentioned in the tags.Erelia
My solution doesn't use jQuery. The jsFiddle only uses jQuery to handle manipulating the elements on which to apply my solution--getting the canvas, handling clicking and window resizing, etc. All of the relevant code is vanilla javascript attached to the CanvasRenderingContext2D prototype.Marplot
E
1

I needed this effect, too a few days ago, and I have managed to create a workaround to achieve it.

What I did was overlay one gradient over the other using something like this:

var ic = [
      /*0*/{ a:"#FEC331" ,b:"#FB1E24"     ,r1:0   ,r2:1   ,x0:0   ,y0:rd*0.5 ,x1:0  ,y1:-rd},
      /*1*/{ a:"#FEC331" ,b:"#FB1E24"     ,r1:0.5 ,r2:0.5 ,x0:0   ,y0:rd*0.3 ,x1:0  ,y1:-rd},
      /*2*/{ a:"#EA6F2B" ,b:"transparent" ,r1:0   ,r2:1   ,x0:-rd ,y0:0      ,x1:rd ,y1:0  }
  ];

Here's the complete code and demo in JSFiddle:

https://jsfiddle.net/flamedenise/n9no9Lgw/33/

Hope it helps.

Eire answered 4/1, 2017 at 11:10 Comment(0)
C
1

2023 Answer:

I know this question has been asked long ago and have been answered, but I thought it would be beneficial for anyone looking for an updated answer to know that in addition to the standard built-in linear and radial gradients, there is now a third - conical gradient - that does exactly what we're looking for without installing the NPM package as mentioned by dx_over_dt in his updated 2021 answer.

Here's the link to the docs.

conical gradient applied to full circle

And a simple example of how it's implemented to achieve the above effect:

<!DOCTYPE html>
<html>

<head>

    <meta charset="utf-8" />
    <title>Conical gradient</title>
    
</head>

<body>
    <div id="container" style="width: 400px; height: 400px">
        <canvas id="circleCanvas" width="400" height="400"></canvas>
    </div>
    
    <script>
        function drawCircle() {
            const canvas = document.getElementById('circleCanvas');
            const ctx = canvas.getContext('2d');

            const centerX = canvas.width / 2;
            const centerY = canvas.height / 2;
            const radius = 200;

            let gradient = ctx.createConicGradient(0, centerX, centerY);

            gradient.addColorStop(0, "red");
            gradient.addColorStop(0.25, "orange");
            gradient.addColorStop(0.5, "yellow");
            gradient.addColorStop(0.75, "green");
            gradient.addColorStop(1, "blue");

            ctx.beginPath();
            ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
            ctx.fillStyle = gradient;
            ctx.fill();
            ctx.closePath();
        }

        window.onload = drawCircle;
    </script>
</body>

Hope it helps!

Consultant answered 13/12, 2023 at 12:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.