在 Canvas 上绘制带有 gradientUnits="objectBoundingBox" 的 SVG 矩形而不使用内置变换函数或更改色标?

Draw an SVG rectangle with gradientUnits="objectBoundingBox" on Canvas without using the built in transform function or changing the color stops?

这是一个使用 objectBoundingBox gradientUnits 的线性渐变的 SVG:

<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>

我需要在 Canvas 上画这个。

如果我使用变换方法,我可以在 Canvas 上绘制渐变:

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);

可惜客户不想让我用transform方式

我可以在 Canvas 上使用家庭滚动缩放绘制相同的渐变。

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);

但是我不能更改色标的百分比。

两种情况下的反对意见都是有效的反对意见,即应该有一个更简单、更优雅的解决方案来解决这个问题。因此,请问这里的聪明人是否有解决方案:

您可以直接使用 drawImage 绘制 SVG:

<canvas id=canvas width=100 height=100></canvas>

<script>
  function svgimage() {
    var image = `
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" version="1.1">
    <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="0" y="0" width="100" height="100" fill="url('#myGradient')" />
</svg>`;
    return encodeURIComponent(image);
  }

  function drawImage() {
    ctx.drawImage(img, 0, 0);
  }

  var canvas = document.querySelector('canvas');
  var ctx = canvas.getContext('2d');

  var img = new Image();
  img.onload = drawImage
  img.src = 'data:image/svg+xml;charset=utf-8,' + svgimage();
</script>

如果您正在寻找坐标变换,这可以解决问题:

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>

就其价值而言,我发现您的 transform 解决方案更优雅。

评论后编辑

按理说,如果我们改变梯度的起点和终点,我们也需要变换梯度步数。我用一个解决方案 ( https://jsfiddle.net/ftadpu3c/3/ ) 对 fiddle 进行了分叉。它使用一个名为 transformGradient 的新函数。由于此转换取决于第一个,因此在 tcoord 中计算了一个参数。我还稍微更改了样式以使其更加一致。传递给 draw 的第三个和第四个参数是宽度和高度,而不是坐标。

编辑 2 我陷入了必须保持变换点到矩形中心的距离的想法。当然,事实并非如此。通过选择合适的距离,就不需要变换梯度了。在 https://jsfiddle.net/uwshL43f/

查看第二个分支