在 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/
查看第二个分支
这是一个使用 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/
查看第二个分支