-6

I have an SVG:

<svg height="781.8" width="1077.15" 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>

which I am trying to reproduce using the Canvas 2D API here:

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

As stated in the comments above, I cannot get the correct center for the gradient & I believe I must have misunderstood the implementation guidelines for SVG. Could I get an explanation as to how to achieve this with userSpaceOnUse or what I am not doing correctly, since using objectBoundingBox works perfectly?

Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
zedsd
  • 11
  • 2
  • canvas gradient is relative to the canvas transform matrix. So by default, to the top left corner of the canvas. You can though move only the fill by first defining your path and then translating the context before actually calling fill(). – Kaiido Jul 08 '21 at 10:53
  • I thought that was what I was actually doing? Am I supposed to just translate instead of using the matrix operation as I am doing? It still doesn't change anything with respect to the center point of the gradient... – zedsd Jul 08 '21 at 15:47

2 Answers2

9

Don't calculate yourself the new coordinates, instead use your context's matrix transform to do this for you and pass directly the same values as in your SVG to the context's methods.

I am a bit lazy, so I won't go and rewrite all your values, instead I'll use a simpler shape, but reintroduce a real rotation:

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 width="200" height="200">
  <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>

Note that we could avoid converting all the <path>'s d commands to their corresponding canvas methods by using a Path2D object, which does accept the same syntax as the d attribute.
However, it's a bit complicated to work with the transformation matrix and Path2D, though not impossible, as shown in this answer of mine.
The basic idea being to create a copy of the Path2D object, transformed by the inverted paint matrix, and then apply the original paint matrix on the context and draw that transformed path.
DOMMatrix objects can help greatly here, but truth be told, it may still look complicated:

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');
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • I appreciate the help, but this unfortunately does not solve the problem because the values in the SVG are generated & cannot be manually adjusted. Using the Path2D API is not supported across all browsers so I cannot use it either. I will attach the solution below & explain why I was not able to reproduce the SVG by applying the transforms directly (calculating). – zedsd Jul 14 '21 at 17:49
  • "because the values in the SVG are generated and cannot be manually adjusted": what do you mean? How is this a problem? I'm telling you to use these values directly onstead of trying to do the traansforms yourself. If you were able to compute these transformed values, obviously you should be able to get the original ones. "Path2D API is not supported across all browsers" only IE doesn't support it, and if you really need to support IE, there are polyfills out there. – Kaiido Jul 14 '21 at 22:11
  • The original values are in the SVG provided. You proposed another shape entirely so this is a problem because I cannot swap the shape with another in the process of rendering the SVG. My question was whether or not an SVG could be implemented on the canvas with transformations applied directly by pre-computing values & with userSpaceOnUse gradients. MDN labels the Path2D API as experimental & I have not seen it being widely adopted, so I cannot use it in production without writing code in case the framework is not present or works incorrectly. – zedsd Jul 15 '21 at 07:32
  • 7
    Did you actually read the two sentences I wrote in my answer? I am using a different shape **only** because I am too lazy to rewrite all the commands from your original file. You just have to do it yourself! So you replace the `[[100, 550],[250, 700],[50, 700]].forEach((pt) => c.lineTo(...pt) );` in my answer with this utterly long `c.moveTo(161.0 -139.65);c.lineTo(160.25 -139.45)...` commands. And Path2D is a stable technology, I don't know why it's still marked as experimental in MDN. The specs are well standardized accross all (but dead IE) browsers. – Kaiido Jul 15 '21 at 07:52
  • 7
    And for the last time, producing a minimal repro is here to make the point of the answer clearer, to not loose the future readers (for whom I do write) into interminable unrelated lines of code. Being lazy is one of the properties of a good programmer. – Kaiido Jul 18 '21 at 23:34
-3

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

Above, is the correctly rendered JS using the Canvas API that will reflect the exact implementation of the SVG in this question. I have posted this solution for others in case they run into the same problem. The reason why it was not working above is because the browser keeps track of the transform matrix & does not simply multiply & discard it (I was doing it to improve performance) so simply applying the transforms directly to the path [although obvious] will not yield the correct user space matrix required to render [& in this case, scale] the gradient properly. The tricky part of manually scaling an SVG is in actually sniffing out any transforms & adjusting them accordingly -- which was the next step. This is effectively what happens when the viewBox attribute of the SVG is used to change the coordinate system of the image. Transforms in general should be processed in order & after any initial adjustments in the SVG coordinate system. It can be done directly in the path values however, I made the mistake of doing them all at once instead of several passes; in particular where the gradient was being applied -- which is where storing the original transformation matrix would have helped.

zedsd
  • 11
  • 2