2

I am trying to rotate a gradient around its centre point of a canvas but I am having no luck in finding the correct calculations that need to be made in order to achieve this.

An example of what I want to do is: http://victorblog.com/html5-canvas-gradient-creator/. The rotation slider is exactly what I want to do but I have looked at the source code and the logic seems like it could be simplified alot using cos/sin (alhough I am no expert in this, hence why I am asking this question).

I found this SO thread (Calculate rotation of canvas gradient) which helped a bit but this revolves around the centre point rather than sticking to it as the rotation slider does in the first example.

Any helpers on this would be much appreciated.

Thanks

danwoodbury
  • 311
  • 1
  • 11

1 Answers1

3

Rotating a Gradient.


Simple rotation.

If you are not worried about the gradient fitting the canvas then a simple rotation will solve the problem.

First you must work out the max length of the gradient so that when it is diagonal it will still fit the canvas.

const maxLength = Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height);

Then you can create the gradient as follows

var angle = ?; // The angle in radians
               // A value of 0 if a gradient from right to left
const gradient = ctx.createLinearGradient(
     // the start of the gradient added to the center
     canvas.width / 2 + Math.cos(angle) * maxLength * 0.5,
     canvas.height / 2 + Math.sin(angle) * maxLength * 0.5,
     // the end of the gradient subtracted from the center
     canvas.width / 2 - Math.cos(angle) * maxLength * 0.5,
     canvas.height / 2 - Math.sin(angle) * maxLength * 0.5
)

This works but will clip the gradient when it is not along the diagonal line.

Fit diagonal example

const eachOf=(a,cb)=>{var i=0;const len=a.length;while(i<len)cb(a[i], i++);};
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const maxWidth = Math.sqrt(w * w + h * h) / 2;
// empty colour items are skipped when creating the gradient
const gradientColours = ["white","blue",,,,"green","yellow","green",,,,"cyan","black"];
function createRotatedGradient(angle, colors){
    const g = ctx.createLinearGradient(
        w / 2 + Math.cos(angle) * maxWidth,  // start pos
        h / 2 + Math.sin(angle) * maxWidth,      
        w / 2 - Math.cos(angle) * maxWidth,  // end pos    
        h / 2 - Math.sin(angle) * maxWidth      
    );
    // add colours
    eachOf(colors,(col,i)=> col && g.addColorStop(i / (colors.length - 1), col) );
    return g;
}

function update(timer){
    ctx.fillStyle = createRotatedGradient(timer / 1000,gradientColours);
    ctx.fillRect(0,0,w,h);
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas><

This can be modified to fit either the width or height by setting the maxWidth of the gradient to either the canvas.height or canvas.width

Fit Height example

const eachOf=(a,cb)=>{var i=0;const len=a.length;while(i<len)cb(a[i], i++);};
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const maxWidth = h / 2;
// empty colour items are skipped when creating the gradient
const gradientColours = ["white","green",,,,"blue","cyan","blue",,,,"yellow","black"];
function createRotatedGradient(angle, colors){
    const g = ctx.createLinearGradient(
        w / 2 + Math.cos(angle) * maxWidth,  // start pos
        h / 2 + Math.sin(angle) * maxWidth,      
        w / 2 - Math.cos(angle) * maxWidth,  // end pos    
        h / 2 - Math.sin(angle) * maxWidth      
    );
    // add colours
    eachOf(colors,(col,i)=> col && g.addColorStop(i / (colors.length - 1), col) );
    return g;
}

function update(timer){
    ctx.fillStyle = createRotatedGradient(timer / 1000,gradientColours);
    ctx.fillRect(0,0,w,h);
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas><

Fit Width example

const eachOf=(a,cb)=>{var i=0;const len=a.length;while(i<len)cb(a[i], i++);};
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const maxWidth = w / 2;
// empty colour items are skipped when creating the gradient
const gradientColours = ["white","blue",,,,"yellow","green","yellow",,,,"cyan","black"];
function createRotatedGradient(angle, colors){
    const g = ctx.createLinearGradient(
        w / 2 + Math.cos(angle) * maxWidth,  // start pos
        h / 2 + Math.sin(angle) * maxWidth,      
        w / 2 - Math.cos(angle) * maxWidth,  // end pos    
        h / 2 - Math.sin(angle) * maxWidth      
    );
    // add colours
    eachOf(colors,(col,i)=> col && g.addColorStop(i / (colors.length - 1), col) );
    return g;
}

function update(timer){
    ctx.fillStyle = createRotatedGradient(timer / 1000,gradientColours);
    ctx.fillRect(0,0,w,h);
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas><

Simple dynamic fit

To fit both the width and height you can use a very simple scaling of the y axis. This scale is the ratio of the width and height.

const maxLen = canvas.width;
const aspect = canvas.height / canvas.width;
const angle = ?
const gradient = ctx.createLinearGradient(
     // the start of the gradient added to the center
     canvas.width / 2 + Math.cos(angle) * maxLen * 0.5,
     canvas.height / 2 + Math.sin(angle) * maxLen * 0.5 * aspect,
     // the end of the gradient subtracted from the center
     canvas.width / 2 - Math.cos(angle) * maxLen * 0.5,
     canvas.height / 2 - Math.sin(angle) * maxLen * 0.5 * aspect
)

Example of aspect scaling

const eachOf=(a,cb)=>{var i=0;const len=a.length;while(i<len)cb(a[i], i++);};
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const maxWidth = w / 2;
const aspect = h / w;
// empty colour items are skipped when creating the gradient
const gradientColours = ["white","red",,,,"yellow","green","yellow",,,,"red","black"];
function createRotatedGradient(angle, colors){
    const g = ctx.createLinearGradient(
        w / 2 + Math.cos(angle) * maxWidth,  // start pos
        h / 2 + Math.sin(angle) * maxWidth * aspect,      
        w / 2 - Math.cos(angle) * maxWidth,  // end pos    
        h / 2 - Math.sin(angle) * maxWidth * aspect      
    );
    // add colours
    eachOf(colors,(col,i)=> col && g.addColorStop(i / (colors.length - 1), col) );
    return g;
}

function update(timer){
    ctx.fillStyle = createRotatedGradient(timer / 1000,gradientColours);
    ctx.fillRect(0,0,w,h);
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas><

Best fit.

The example site you provided fitted the gradient so that it was from the closest edges, this is a better fit but still is not a perfect fit as the gradient will be too short at times. As you have the method from that site I will not include it here.

The best fit is a little more complex but will always fit the canvas so that there is no over or under flow of the canvas ( no pixels will be set from outside the gradient and all of the gradient will be visible at any angle)

See the example for information. The maths was taken and adapted from this answer that fits a rotated image.

Example of best fit.

const eachOf=(a,cb)=>{var i=0;const len=a.length;while(i<len)cb(a[i], i++);};
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const maxWidth = w / 2;
const aspect = h / w;
// empty colour items are skipped when creating the gradient
const gradientColours = ["black","white",,,,"white","red",,,,,,,,,"yellow","green","yellow",,,,,,,,,"red","black",,,,"black","white"];

function bestFitGradient(angle, colors){
    
    var dist = Math.sqrt(w * w + h * h) / 2; // get the diagonal length    
    var diagAngle = Math.asin((h / 2) / dist); // get the diagonal angle

    // Do the symmetry on the angle (move to first quad
    var a1 = ((angle % (Math.PI *2))+ Math.PI*4) % (Math.PI * 2);
    if(a1 > Math.PI){ a1 -= Math.PI }
    if(a1 > Math.PI / 2 && a1 <= Math.PI){ a1 = (Math.PI / 2) - (a1 - (Math.PI / 2)) }
    // get angles from center to edges for along and right of gradient
    var ang1 = Math.PI/2 - diagAngle - Math.abs(a1);
    var ang2 = Math.abs(diagAngle - Math.abs(a1));
    
    // get distance from center to horizontal and vertical edges
    var dist1 = Math.cos(ang1) * h;
    var dist2 = Math.cos(ang2) * w;
    
    // get the max distance
    var scale = Math.max(dist2, dist1) / 2;
    
    // get the vector to the start and end of gradient
    var dx = Math.cos(angle) * scale;
    var dy = Math.sin(angle) * scale;
    
    // create the gradient
    const g = ctx.createLinearGradient(
        w / 2 + dx,  // start pos
        h / 2 + dy,      
        w / 2 - dx,  // end pos    
        h / 2 - dy      
    );
    // add colours
    eachOf(colors,(col,i)=> col && g.addColorStop(i / (colors.length - 1), col) );
    return g;
}



function update(timer){
    ctx.fillStyle = bestFitGradient(timer / 1000,gradientColours);
    ctx.fillRect(0,0,w,h);
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas><
Blindman67
  • 51,134
  • 11
  • 73
  • 136