是否可以使用当前的 MDN 画布 2D API 函数在 userSpaceOnUse 坐标中使用 gradientTransforms 渲染 SVG 径向渐变?

Posted

技术标签:

【中文标题】是否可以使用当前的 MDN 画布 2D API 函数在 userSpaceOnUse 坐标中使用 gradientTransforms 渲染 SVG 径向渐变?【英文标题】:Is it possible to render SVG radial gradients with gradientTransforms in userSpaceOnUse coordinates using the current MDN canvas 2D API functions? 【发布时间】:2021-09-18 16:07:55 【问题描述】:

我有一个 SVG:

<svg   xmlns="http://www.w3.org/2000/svg">
    <radialGradient cx="0" cy="0" gradientTransform="matrix(-0.0664, 0.0141, 0.0063, 0.0288, 137.9, -123.45)" gradientUnits="userSpaceOnUse" id="gradient0" r="819.2" spreadMethod="pad">
      <stop offset="0.0" stop-color="#ff0000" stop-opacity="0.34901962"/>
      <stop offset="1.0" stop-color="#ff0000" stop-opacity="0.0"/>
    </radialGradient>
    <g transform="translate(309.85, 414.55) rotate(0, 600, 300)">
       <path transform="translate(-125, -10)" d="M161.0 -139.65 L160.25 -139.45 160.8 -139.85 Q161.55 -140.4 163.6 -140.15 L161.0 -139.65 M92.55 -115.35 Q91.1 -122.1 97.5 -128.9 107.45 -139.25 134.7 -146.75 L142.55 -148.35 Q147.4 -149.25 152.3 -148.3 L160.45 -147.3 161.4 -147.25 159.25 -145.4 Q152.35 -139.0 152.35 -135.5 L152.4 -134.75 152.4 -134.6 151.85 -132.55 151.6 -131.55 151.65 -131.45 Q152.45 -130.95 153.3 -131.0 L153.55 -130.1 Q153.55 -129.7 153.7 -129.45 154.0 -128.9 155.05 -128.9 158.8 -128.9 162.2 -130.4 164.8 -131.5 165.55 -132.55 L166.7 -134.25 166.75 -134.25 170.0 -136.6 Q171.95 -138.0 172.25 -139.25 L172.1 -140.5 172.1 -141.75 Q172.35 -142.6 172.95 -143.65 173.35 -144.25 173.15 -144.7 L172.9 -145.25 173.0 -145.5 175.35 -144.6 Q179.2 -142.4 180.8 -139.7 181.8 -138.0 182.7 -134.5 184.3 -127.3 179.2 -119.65 170.45 -106.4 143.85 -100.8 135.15 -98.9 123.45 -99.6 110.8 -100.5 101.05 -104.15 95.85 -106.15 94.15 -109.2 93.65 -110.15 92.55 -115.35" fill="url(#gradient0)" fill-rule="evenodd" stroke="none"/>
    </g>
</svg>

我在这里尝试使用 Canvas 2D API 重现:

var cv = document.createElement('canvas');
cv.width = 1077.15;
cv.height = 781.8;
var c = cv.getContext('2d');
document.body.appendChild(cv);
c.translate(600,300);
c.rotate(0);
c.translate(-600,-300);
// The rotation above was set to 0 to exclude it from implementation until this issue is resolved... though if this is not the correct way to rotate using the API, please let me know...
c.translate(80,60);// I'm not sure where I missed out on the math to calculate this offset... I included it to position the path approximately where it should be
c.beginPath();
c.moveTo(256.863018149747,203.30007674597084);
c.lineTo(256.3059926658311,203.45356868764392);
c.lineTo(256.7144780207028,203.14658480429782);
c.quadraticCurveTo(257.2715035046187,202.72448196469685,258.79403982732214,202.91634689178818);
c.lineTo(256.863018149747,203.30007674597084);
c.moveTo(206.0251589843569,221.94934765924793);
c.quadraticCurveTo(204.94824304878617,216.7689946277821,209.70152717820176,211.55026861089792);
c.quadraticCurveTo(217.09139859815252,203.60706062931698,237.32999118042983,197.85111281657714);
c.lineTo(243.16019124541617,196.6231772831927);
c.quadraticCurveTo(246.76228937473888,195.93246354566386,250.40152253632272,196.6615502686109);
c.lineTo(256.4545327948754,197.4290099769762);
c.lineTo(257.1600984078355,197.46738296239448);
c.lineTo(255.56329202060994,198.8871834228703);
c.quadraticCurveTo(250.43865756858378,203.7989255564083,250.43865756858378,206.4850345356869);
c.lineTo(250.4757926008448,207.06062931696087);
c.lineTo(250.4757926008448,207.1757482732157);
c.lineTo(250.06730724597318,208.74904067536454);
c.lineTo(249.8816320846679,209.51650038372986);
c.lineTo(249.91876711692893,209.59324635456642);
c.quadraticCurveTo(250.51292763310587,209.97697620874908,251.1442231815439,209.9386032233308);
c.lineTo(251.3298983428492,210.6293169608596);
c.quadraticCurveTo(251.3298983428492,210.9363008442057,251.44130343963235,211.12816577129703);
c.quadraticCurveTo(251.66411363319872,211.55026861089792,252.44394931068098,211.55026861089792);
c.quadraticCurveTo(255.22907673026043,211.55026861089792,257.75425892401245,210.39907904834996);
c.quadraticCurveTo(259.6852806015875,209.55487336914814,260.2423060855034,208.74904067536454);
c.lineTo(261.0964118275078,207.44435917114353);
c.lineTo(261.13354685976884,207.44435917114353);
c.lineTo(263.54732395673767,205.6408288564851);
c.quadraticCurveTo(264.995590214919,204.56638526477363,265.21840040848537,203.60706062931698);
c.lineTo(265.1069953117022,202.64773599386035);
c.lineTo(265.1069953117022,201.6884113584037);
c.quadraticCurveTo(265.2926704730075,201.0360706062932,265.7382908601402,200.23023791250958);
c.quadraticCurveTo(266.0353711182287,199.76976208749042,265.88683098918443,199.42440521872604);
c.lineTo(265.7011558278791,199.00230237912513);
c.lineTo(265.77542589240124,198.8104374520338);
c.lineTo(267.52077240867106,199.5011511895626);
c.quadraticCurveTo(270.3801698927726,201.18956254796623,271.5684909251265,203.2617037605526);
c.quadraticCurveTo(272.3111915703477,204.56638526477363,272.9796221510467,207.2524942440522);
c.quadraticCurveTo(274.16794318340067,212.77820414428243,270.3801698927726,218.64927091327704);
c.quadraticCurveTo(263.88153924708723,228.81811204911742,244.1257020842037,233.11588641596316);
c.quadraticCurveTo(237.66420647077936,234.57405986185725,228.9746089216915,234.0368380660016);
c.quadraticCurveTo(219.57944575964353,233.34612432847277,212.33811446873696,230.54489639293936);
c.quadraticCurveTo(208.4760711135868,229.00997697620875,207.21348001671075,226.6692248656946);
c.quadraticCurveTo(206.84212969410015,225.9401381427475,206.0251589843569,221.94934765924793);
c.closePath();
var gradient=c.createRadialGradient(206.0251589843569,196.6231772831927,0,206.0251589843569,196.6231772831927,819.2*66.95446316668983);// Obviously this cannot be the correct conversion from the bounding box (x:206.0251589843569, y:196.6231772831927, w:66.95446316668983, h:37.413660782808904) to user system coordinates..
gradient.addColorStop(0,'rgba(255,0,0,0.34901962)');
gradient.addColorStop(1,'rgba(255,0,0,0)');
c.transform(-0.0664,0.0141,0.0063,0.0288,137.9,-123.45);
c.fillStyle=gradient;
c.fill('evenodd');

如上面的 cmets 所述,我无法获得正确的渐变中心,我相信我一定误解了 SVG 的实施指南。我能否解释一下如何使用 userSpaceOnUse 来实现这一点,或者我做错了什么,因为使用 objectBoundingBox 效果很好?

【问题讨论】:

画布渐变是相对于画布变换矩阵的。所以默认情况下,在画布的左上角。您可以通过首先定义路径然后在实际调用 fill() 之前翻译上下文来仅移动填充。 我以为这就是我实际在做的事情?我应该只是翻译而不是像我一样使用矩阵运算吗?关于渐变的中心点,它仍然没有改变任何东西...... 【参考方案1】:

不要自己计算新坐标,而是使用上下文的矩阵变换为您执行此操作,并将与 SVG 中相同的值直接传递给上下文的方法。

我有点懒,所以我不会去重写你所有的值,而是使用更简单的形状,但重新引入真正的旋转:

const cv = document.createElement('canvas');
cv.width = 200;
cv.height = 200;
const c = cv.getContext('2d');
document.body.appendChild(cv);

// the <g> transform (in order)
c.translate(-50, -500);
// rotate with transform origin
c.translate(150, 700);
c.rotate((Math.PI / 180) * 30)
c.translate(-150, -700);

// the path drawing (relative to the <g>)
c.beginPath();
[[100, 550],[250, 700],[50, 700]]
  .forEach((pt) => c.lineTo(...pt) );
c.closePath();

// same values as in the SVG (cx, cy, 0, cx, cy, rad)
const gradient = c.createRadialGradient(0, 0, 0, 0, 0, 2000);
gradient.addColorStop(0,'rgba(255,0,0,0.34901962)');
gradient.addColorStop(1,'rgba(255,0,0,0)');
c.fillStyle = gradient;

// now we add the gradient's transform to the context's one
c.transform(-0.0664, 0.0141, 0.0063, 0.0288, 150, 600);
// we can finally paint
c.fill('evenodd');
svg 
  border: 1px solid blue;

canvas 
  border: 1px solid green;
<svg  >
  <radialGradient id="gradient0"
      cx="0" cy="0" r="2000"
      gradientTransform="matrix(-0.0664, 0.0141, 0.0063, 0.0288, 150, 600)"
      gradientUnits="userSpaceOnUse" spreadMethod="pad">
    <stop offset="0.0" stop-color="#ff0000" stop-opacity="0.3"/>
    <stop offset="1.0" stop-color="#ff0000" stop-opacity="0.0"/>
  </radialGradient>
  <g transform="translate(-50, -500) rotate(30, 150, 700)">
    <path d="M100 550L250 700L50 700Z" fill="url(#gradient0)"/>
  </g>
</svg>

请注意,我们可以通过使用Path2D 对象来避免将所有&lt;path&gt;d 命令转换为其相应的画布方法,该对象接受与d 属性相同的语法。 但是,使用变换矩阵和 Path2D 有点复杂,尽管并非不可能,如 this answer of mine 所示。 基本思想是创建 Path2D 对象的副本,由倒置的绘制矩阵转换,然后将原始绘制矩阵应用于上下文并绘制转换后的路径。 DOMMatrix 对象在这里可以提供很大帮助,但说实话,它可能看起来仍然很复杂:

var cv = document.createElement('canvas');
cv.width = 1077;
cv.height = 782;
var c = cv.getContext('2d');
document.body.appendChild(cv);

const path = new Path2D( "M161.0 -139.65 L160.25 -139.45 160.8 -139.85 Q161.55 -140.4 163.6 -140.15 L161.0 -139.65 M92.55 -115.35 Q91.1 -122.1 97.5 -128.9 107.45 -139.25 134.7 -146.75 L142.55 -148.35 Q147.4 -149.25 152.3 -148.3 L160.45 -147.3 161.4 -147.25 159.25 -145.4 Q152.35 -139.0 152.35 -135.5 L152.4 -134.75 152.4 -134.6 151.85 -132.55 151.6 -131.55 151.65 -131.45 Q152.45 -130.95 153.3 -131.0 L153.55 -130.1 Q153.55 -129.7 153.7 -129.45 154.0 -128.9 155.05 -128.9 158.8 -128.9 162.2 -130.4 164.8 -131.5 165.55 -132.55 L166.7 -134.25 166.75 -134.25 170.0 -136.6 Q171.95 -138.0 172.25 -139.25 L172.1 -140.5 172.1 -141.75 Q172.35 -142.6 172.95 -143.65 173.35 -144.25 173.15 -144.7 L172.9 -145.25 173.0 -145.5 175.35 -144.6 Q179.2 -142.4 180.8 -139.7 181.8 -138.0 182.7 -134.5 184.3 -127.3 179.2 -119.65 170.45 -106.4 143.85 -100.8 135.15 -98.9 123.45 -99.6 110.8 -100.5 101.05 -104.15 95.85 -106.15 94.15 -109.2 93.65 -110.15 92.55 -115.35" );

const gradient = c.createRadialGradient( 0, 0, 0, 0, 0, 819.2 );
gradient.addColorStop(0,'rgba(255,0,0,0.34901962)');
gradient.addColorStop(1,'rgba(255,0,0,0)');

// the gradient matrix, inversed,
// to be used when generating our final Path2D object
const grad_mat = new DOMMatrix("matrix(-0.0664, 0.0141, 0.0063, 0.0288, 137.9, -123.45)").inverse();
const transformed_path = new Path2D();
transformed_path.addPath( path, grad_mat );

// the context needs to have the inverted transform
const context_mat = new DOMMatrix();
// we need to add the <g>'s transformation
context_mat.translateSelf(184.85, 404.55);
// and the gradient's
context_mat.multiplySelf( grad_mat.inverse() );

c.setTransform( context_mat );
c.fillStyle = gradient;
c.fill(transformed_path, 'evenodd');

【讨论】:

感谢您的帮助,但不幸的是,这并不能解决问题,因为 SVG 中的值是生成的并且无法手动调整。并非所有浏览器都支持使用 Path2D API,因此我也无法使用它。我将附上下面的解决方案并解释为什么我无法通过直接应用转换(计算)来重现 SVG。 “因为SVG中的值是生成的,不能手动调整”:什么意思?这是怎么回事?我告诉您直接使用这些值,而不是尝试自己进行转换。如果您能够计算这些转换后的值,显然您应该能够获得原始值。 “不是所有浏览器都支持 Path2D API”只有 IE 不支持它,如果你真的需要支持 IE,那里有 polyfills。 原始值在提供的 SVG 中。您完全提出了另一个形状,所以这是一个问题,因为在渲染 SVG 的过程中我无法将形状与另一个形状交换。我的问题是是否可以在画布上实现 SVG,并通过预计算值直接应用转换并使用 userSpaceOnUse 渐变。 MDN 将 Path2D API 标记为实验性的,我还没有看到它被广泛采用,所以我不能在不编写代码的情况下在生产中使用它,以防框架不存在或工作不正确。 你真的看过我在回答中写的两句话吗?我使用不同的形状 only 因为我懒得重写原始文件中的所有命令。你只需要自己做!因此,您将我的答案中的[[100, 550],[250, 700],[50, 700]].forEach((pt) =&gt; c.lineTo(...pt) ); 替换为这个非常长的c.moveTo(161.0 -139.65);c.lineTo(160.25 -139.45)... 命令。而且Path2D是一种稳定的技术,我不知道为什么在MDN中它仍然被标记为实验性的。这些规范在所有(除了死的 IE)浏览器中都得到了很好的标准化。 最后一次,制作一个最小的重现是为了让答案的重点更清楚,不要让未来的读者(我为他们写的)迷失在无休止的不相关的代码行中。懒惰是优秀程序员的特质之一。【参考方案2】:

<!DOCTYPE html>
<head><title>Test</title></head><body>
<script>
var cv = document.createElement('canvas');
cv.width = 1077.15;
cv.height = 781.8;
document.body.appendChild(cv);
var c = cv.getContext('2d');
c.globalAlpha=1;
c.setTransform(1,0,0,1,0,0);
c.transform(1,0,0,1,-124.98259295362763,-9.989767203888464);
c.transform(1,0,0,1,309.8068514134522,414.12579943719624);
c.translate(600,300);
c.rotate(0);
c.translate(-600,-300);
c.translate(600,300);
c.rotate(0);
c.translate(-600,-300);
c.beginPath();
c.moveTo(160.97757972427237,-139.5070990023024);
c.lineTo(160.2276841665506,-139.3073036582246);
c.lineTo(160.7776075755466,-139.70689434638015);
c.quadraticCurveTo(161.52750313326834,-140.25633154259404,163.57721765770782,-140.00658736249682);
c.lineTo(160.97757972427237,-139.5070990023024);
c.moveTo(92.5371118228659,-115.23196469685342);
c.quadraticCurveTo(91.0873137446038,-121.97505755947813,97.48642250382954,-128.7680992581223);
c.quadraticCurveTo(107.43503690293831,-139.10750831414686,134.68124216682912,-146.5998337170632);
c.lineTo(142.53014900431697,-148.19819646968534);
c.quadraticCurveTo(147.3794736109177,-149.0972755180353,152.2787912546999,-148.14824763366593);
c.lineTo(160.4276563152764,-147.14927091327706);
c.lineTo(161.37752402172399,-147.09932207725763);
c.lineTo(159.2278234229216,-145.25121514453826);
c.quadraticCurveTo(152.32878429188133,-138.85776413404963,152.32878429188133,-135.36134561268867);
c.lineTo(152.3787773290628,-134.61211307239705);
c.lineTo(152.3787773290628,-134.4622665643387);
c.lineTo(151.82885392006682,-132.4143642875416);
c.lineTo(151.5788887341596,-131.41538756715275);
c.lineTo(151.62888177134104,-131.31548989511384);
c.quadraticCurveTo(152.42877036624424,-130.81600153491942,153.27865199832894,-130.86595037093886);
c.lineTo(153.5286171842362,-129.9668713225889);
c.quadraticCurveTo(153.5286171842362,-129.56728063443336,153.67859629578052,-129.31753645433614);
c.quadraticCurveTo(153.97855451886923,-128.7680992581223,155.0284082996797,-128.7680992581223);
c.quadraticCurveTo(158.77788608828854,-128.7680992581223,162.1774126166272,-130.26656433870556);
c.quadraticCurveTo(164.77705055006268,-131.36543873113328,165.52694610778443,-132.4143642875416);
c.lineTo(166.67678596295778,-134.11262471220263);
c.lineTo(166.72677900013926,-134.11262471220263);
c.lineTo(169.97632641693357,-136.4602200051164);
c.quadraticCurveTo(171.92605486701015,-137.8587874136608,172.22601309009886,-139.10750831414686);
c.lineTo(172.0760339785545,-140.35622921463292);
c.lineTo(172.0760339785545,-141.60495011511895);
c.quadraticCurveTo(172.32599916446176,-142.45408032744947,172.92591561063918,-143.50300588385778);
c.quadraticCurveTo(173.3258599080908,-144.10239191609108,173.12588775936499,-144.55193144026606);
c.lineTo(172.87592257345773,-145.10136863647992);
c.lineTo(172.97590864782063,-145.35111281657714);
c.lineTo(175.32558139534882,-144.45203376822718);
c.quadraticCurveTo(179.17504525832055,-142.25428498337172,180.774822448127,-139.5570478383218);
c.quadraticCurveTo(181.77468319175603,-137.8587874136608,182.67455786102212,-134.36236889229983);
c.quadraticCurveTo(184.27433505082857,-127.16973650550014,179.17504525832055,-119.52756459452547);
c.quadraticCurveTo(170.4262637515666,-106.29112304937325,143.82996797103468,-100.6968534151957);
c.quadraticCurveTo(135.13117950146219,-98.7987976464569,123.43280880100265,-99.4980813507291);
c.quadraticCurveTo(110.78457039409552,-100.39716039907906,101.03592814371257,-104.04342542849835);
c.quadraticCurveTo(95.83665227684166,-106.04137886927604,94.13688901267233,-109.08825786646202);
c.quadraticCurveTo(93.63695864085783,-110.03728575083143,92.5371118228659,-115.23196469685342);
c.closePath();
var gradient=c.createRadialGradient(0,0,0,0,0,819.2);
gradient.addColorStop(0,'rgba(255,0,0,0.34901962)');
gradient.addColorStop(1,'rgba(255,0,0,0)');
c.fillStyle=gradient;
c.transform(0.999860743629021,0,0,0.9989767203888463,0,0);
c.transform(-0.0664,0.0141,0.0063,0.0288,137.9,-123.45);
c.fill('evenodd');
c.rotate(0);
</script></body><html>

上面是使用 Canvas API 正确呈现的 JS,它将反映该问题中 SVG 的确切实现。我已经为其他人发布了这个解决方案,以防他们遇到同样的问题。它在上面不起作用的原因是因为浏览器会跟踪变换矩阵并且不会简单地乘以和丢弃它(我这样做是为了提高性能)所以简单地将变换直接应用于路径 [虽然很明显] 不会产生正确渲染所需的用户空间矩阵 [& 在这种情况下,缩放] 渐变。手动缩放 SVG 的棘手部分实际上是嗅出任何变换并相应地调整它们——这是下一步。当 SVG 的 viewBox 属性用于更改图像的坐标系时,实际上会发生这种情况。通常应在 SVG 坐标系中进行任何初始调整后按顺序处理变换。它可以直接在路径值中完成,但是我犯了一次完成所有这些而不是多次传递的错误;特别是在应用梯度的地方——存储原始变换矩阵会有帮助。

【讨论】:

以上是关于是否可以使用当前的 MDN 画布 2D API 函数在 userSpaceOnUse 坐标中使用 gradientTransforms 渲染 SVG 径向渐变?的主要内容,如果未能解决你的问题,请参考以下文章

Canvas API

前端笔记 — canvas

canvas钟表

一个很强大canvas库(Fabric)

canvas 2D API的1.2 toDataURL()方法

html5中Canvas为什么要用getContext('2d')