Here is an SVG that has a linear gradient that uses objectBoundingBox gradientUnits:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="myGradient" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="objectBoundingBox">
<stop offset="40%" stop-color="yellow" />
<stop offset="50%" stop-color="black" />
<stop offset="60%" stop-color="red" />
</linearGradient>
</defs>
<rect x="10" y="10" width="20" height="10" fill="url('#myGradient')" />
</svg>
I need to draw this on a Canvas.
I can draw the gradient on a Canvas if I use the transform method:
const canvas = document.getElementById('canvasBuiltInScale');
const ctx = canvas.getContext('2d');
function draw(x0, y0, x1, y1) {
ctx.save();
// create a square 1x1 gradient
const gradient = ctx.createLinearGradient(0, 0, 1, 1);
gradient.addColorStop(0.4, 'yellow');
gradient.addColorStop(0.5, 'black');
gradient.addColorStop(0.6, 'red');
// scale it up to the size of the bbox
const width = x1 - x0;
const height = y1 - y0;
ctx.transform(width, 0, 0, height, x0, y0);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, 1);
ctx.restore();
}
draw(10, 10, 40, 30);
But unfortunately the customer does not want me to use the transform method.
I can draw the same gradient on a Canvas with a home rolled scaling instead.
const canvas = document.getElementById('canvasHomeRolledScale');
const ctx = canvas.getContext('2d');
function draw(x0, y0, x1, y1) {
const width = x1-x0;
const height = y1-y0;
// The problem is that with userSpace coordinates, the normal to the gradient vector from x0,y0 to x1,y1 will not go between x1,y0 and x0,y1
// I perform a home baked geometric calculation to find the normal vector to [x1-x0, y1-y0] since its normal vector will pass through [x1-x0, y1-y0]
const gradient = ctx.createLinearGradient(
x0 + (width - height) / 2,
y0 + (height - width) / 2,
x0 + (width - height) / 2 + height,
y0 + (height - width) / 2 + width
);
gradient.addColorStop(rescale(0.4), 'yellow');
gradient.addColorStop(rescale(0.5), 'black');
gradient.addColorStop(rescale(0.6), 'red');
ctx.fillStyle = gradient;
ctx.fillRect(x0, y0, width, height);
// The normal vector calculated above has the right direction, but not the right amplitude.
// Here I guy guessed that I could use pythagoras theorem to arrive at the correct scale
function rescale(percent) {
const max = Math.max(height, width)
const min = Math.min(height, width)
const f = (
Math.sqrt(
Math.pow(max, 2) + Math.pow(min, 2)
) /
Math.sqrt(
Math.pow(max, 2) + Math.pow(max, 2)
)
);
const midPoint = 0.5;
return midPoint - (midPoint-percent) * f
}
}
draw(10, 10, 40, 30);
But I am not allowed to change the percentage of the color stops.
The objection in both cases has been the valid objection that there ought to be a simpler more elegant solution to this problem. Hence, I ask the intelligent people in here if there is a solution that:
If you are looking for a transformation of coordinates, this would do the trick:
const canvas = document.getElementById('canvasBuiltInScale');
const ctx = canvas.getContext('2d');
function tcoord(x0, y0, x1, y1){
let xc = (x1 + x0) / 2;
let yc = (y1 + y0) / 2;
let dx = (x1 - x0) / 2;
let dy = (y1 - y0) / 2;
let rx0 = xc - dy;
let ry0 = yc - dx;
let rx1 = xc + dy;
let ry1 = yc + dx;
let result = [rx0,ry0,rx1,ry1];
return result;
}
function draw(x0, y0, x1, y1) {
ctx.save();
let c = tcoord(x0, y0, x0 + x1, y0 + y1);
const gradient = ctx.createLinearGradient(c[0], c[1], c[2], c[3]);
gradient.addColorStop(0.4, 'yellow');
gradient.addColorStop(0.5, 'black');
gradient.addColorStop(0.6, 'red');
ctx.fillStyle = gradient;
ctx.fillRect(x0, y0, x1, y1);
ctx.restore();
}
draw(10, 10, 80, 60);
<canvas id="canvasBuiltInScale" width="300" height="300">
</canvas>
For what it is worth, I find your transform
solution way more elegant.
EDIT after comment
It stands to reason that if we change the start and end points of the gradient, we also need to transform the gradient steps. I have forked the fiddle with a solution ( https://jsfiddle.net/ftadpu3c/3/ ). It uses a new function called transformGradient
. Since this transformation depends on the first one, there is one parameter that is calculated in tcoord
. I also changed the style a little bit to make it more consistent. The third and fourth parameters passed to draw
are a width and a height, rather than coordinates.
Edit 2 I was caught up in the notion that the distance of the transformed points to the center of the rectangle had to be maintained. Of course, that is not true. By choosing a suitable distance, it will not be necessary to transform the gradient. See second fork at https://jsfiddle.net/uwshL43f/