4

These two SVGs have linear gradients that are expressed in different coordinate systems, but render the same image. I would like to be able to convert between these coordinate systems. I know how to convert from objectBoundingBox to userSpaceOnUse, but not the other direction.

<svg xmlns="http://www.w3.org/2000/svg">
 <defs>
    <linearGradient id="myGradient" x1="80" y1="35" x2="120" y2="115" gradientUnits="userSpaceOnUse">
      <stop offset="40%" stop-color="yellow" />
      <stop offset="50%" stop-color="black" />
      <stop offset="60%" stop-color="red" />
    </linearGradient>
  </defs>

  W<rect x="50" y="50" width="100" height="50" fill="url('#myGradient')" />
</svg>

<svg 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="50" y="50" width="100" height="50" fill="url('#myGradient')" />
</svg>

In the example below toUserSpaceOnUse converts the coordinates of an SVG gradient from objectBoundingBox to userSpaceOnUse. How would a function look that does the opposite, converts from userSpaceOnUse to objectBoundingBox coordinates, toObjectBoundingBox?

    draw()
    
    function draw() {
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
    
      function toUserSpaceOnUse(x0, y0, w, h){
        let x1 = x0 + w;
        let y1 = y0 + h;
        let gtransform = 2 / (w / h + h / w);
        let xc = (x1 + x0) / 2;
        let yc = (y1 + y0) / 2;
        let dx = gtransform * (x1 - x0) / 2;
        let dy = gtransform * (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, w, h) {
        ctx.save();
        let c = toUserSpaceOnUse(x0, y0, w, h);
        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, w, h);  
        ctx.restore();
      }
    
      draw(50, 50, 100, 50);
    }
    <div>
      <p>
        With objectBoundingBox coordinates
      </p>
      <svg 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="50" y="50" width="100" height="50" fill="url('#myGradient')" />
    </svg>
    </div>
    
    <div>
      <p>
        With userSpaceOnUse coordinates
      </p>
      <canvas id="canvas" />
    </div>
user1283776
  • 19,640
  • 49
  • 136
  • 276
  • 1
    Your function cannot do what you say it does. Firstly if you only have objectBoundingBox coordinates values you can't convert them to userSpaceOnUse coordinates without extra information. Secondly, you are subtracting y values from x coordinates and vice versa. That can't be right. – Paul LeBeau Jun 17 '20 at 15:14
  • 2
    To convert from objectBoundingBox (x,y), to userspace, you need to know the x, y, width, and height of the element it applies to. For example, your element has `x="50" y="50" width="200" height="200"`, so an objectBoundingBox coordinate value of 0.25, would correspond to `50 + (0.25 * 200) = 100`. – Paul LeBeau Jun 17 '20 at 15:20
  • @PaulLeBeau: I have clarified my question. I don't know how I could use you comments. Do you think you could expand your comments into a full answer? – user1283776 Jun 22 '20 at 07:51
  • @vqf: You are extremely welcome to look at this question if you have time. – user1283776 Jun 22 '20 at 10:54

1 Answers1

6

I think I understand what you were trying to do now. You were assuming that the gradient coordinates were always 0% 0% 100% 100%, and then trying to calculate absolute gradient coordinates that simulates the "stretch" that the objectBoundingBox transform produces.

There is an a much easier way to do that. There is no need for a complicated calculation function. See below.

draw()
    
    function draw() {
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');

      function draw(x0, y0, w, h) {
        ctx.save();
        const gradient = ctx.createLinearGradient(0, 0, 1, 1); // 0% 0% 100% 100%
        gradient.addColorStop(0.4, 'yellow');
        gradient.addColorStop(0.5, 'black');
        gradient.addColorStop(0.6, 'red');
        ctx.fillStyle = gradient;
        ctx.translate(x0, y0);  // )
        ctx.scale(w, h);        // ) simulates the objectBoundingBox->userSpaceOnUse transform
        ctx.fillRect(0, 0, 1, 1);
        ctx.restore();
      }
    
      draw(50, 50, 100, 50);
    }
<div>
      <p>
        With objectBoundingBox coordinates
      </p>
      <svg 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="50" y="50" width="100" height="50" fill="url('#myGradient')" />
    </svg>
    </div>
    
    <div>
      <p>
        With userSpaceOnUse coordinates
      </p>
      <canvas id="canvas" />
    </div>

Does that help?

Why do you need to go the other way (from userSpaceOnUse to objectBoundingBox)? Is your ultimate goal to render to an HTML Canvas, or something else? If I can understand what you are after, I will be better able to answer your question.

Update

Here is the reverse function you were after.

I started by modifying your original function to support objectBoundingBox coords other than (0% 0% 100% 100%).

Then for the reverse function, it is basically just a matter of reversing the operations of the original function.

draw()

function draw() {
  const grad = document.getElementById('myGradient2');

  // Convert objectBoundingBox coords to their userspace equivalents, compensating for the obb transform
  // x0,y0,w,h are the element (rect) attributes
  // o_x0, o_y0, o_x1, o_y1 are the objectBoundingBox coords
  function toUserSpaceOnUse(x0, y0, w, h, o_x0, o_y0, o_x1, o_y1) {
    // Convert objectBoundingBox coords (o_*) to userspace coords (u_*)
    let u_x0 = x0 + o_x0 * w;  
    let u_y0 = y0 + o_y0 * h;
    let u_x1 = x0 + o_x1 * w;
    let u_y1 = y0 + o_y1 * h;
    // Now recalculate the coords to simulate the effect of the objectBoundingBox implicit transformation
    let gtransform = 2 / (w / h + h / w);
    let xc = (u_x1 + u_x0) / 2;
    let yc = (u_y1 + u_y0) / 2;
    let dx = gtransform * (u_x1 - u_x0) / 2;
    let dy = gtransform * (u_y1 - u_y0) / 2;
    let rx0 = xc - dy;
    let ry0 = yc - dx;
    let rx1 = xc + dy;
    let ry1 = yc + dx;
    return [rx0,ry0,rx1,ry1];
  }

  // Convert userspace coords to their objectBoundingBox equivalents, compensating for the obb transform
  // x0,y0,w,h are the element (rect) attributes
  // u_x0, u_y0, u_x1, u_y1 are the userspace coords
  function toObjectBoundingBox(x0, y0, w, h, u_x0, u_y0, u_x1, u_y1) {
    // Recalculate the coords to simulate the effect of the reverse objectBoundingBox implicit transformation
    let gtransform = 2 / (w / h + h / w);
    let xc = (u_x1 + u_x0) / 2;
    let yc = (u_y1 + u_y0) / 2;
    let dx = (xc - u_x0) / gtransform;
    let dy = (yc - u_y0) / gtransform;
    let _x0 = xc - dy;
    let _y0 = yc - dx;
    let _x1 = xc + dy;
    let _y1 = yc + dx;
    // Convert userspace coords (u_*) to objectBoundingBox coords (o_*)
    let o_x0 = (_x0 - x0) / w;
    let o_y0 = (_y0 - y0) / h;
    let o_x1 = (_x1 - x0) / w;
    let o_y1 = (_y1 - y0) / h;
    return [o_x0, o_y0, o_x1, o_y1];
  }

  function draw(x0, y0, w, h, u_x0, u_y0, u_x1, u_y1) {
    let d = toObjectBoundingBox(x0, y0, w, h, u_x0, u_y0, u_x1, u_y1)
    grad.setAttribute("x1", d[0]);
    grad.setAttribute("y1", d[1]);
    grad.setAttribute("x2", d[2]);
    grad.setAttribute("y2", d[3]);
  }

  draw(50, 50, 100, 50, 80, 35, 120, 115);

  /*
  let a = [0.1, 0.2, 0.7, 0.8];
  let b = toUserSpaceOnUse(50, 50, 100, 50, ...a);
  let c = toObjectBoundingBox(50, 50, 100, 50, ...b);
  console.log("These should match: ",a,c);
  */
}
<div>
  <p>
    With objectBoundingBox coordinates
  </p>
  <svg xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="myGradient" x1="80" y1="35" x2="120" y2="115" gradientUnits="userSpaceOnUse">
        <stop offset="40%" stop-color="yellow" />
        <stop offset="50%" stop-color="black" />
        <stop offset="60%" stop-color="red" />
      </linearGradient>
    </defs>
    
    <rect x="50" y="50" width="100" height="50" fill="url('#myGradient')" />
  </svg>
</div>
    
<div>
  <p>
    With userSpaceOnUse coordinates
  </p>
  <svg xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="myGradient2" x1="0%" y1="0%" x2="0%" y2="0%" gradientUnits="objectBoundingBox">
        <stop offset="40%" stop-color="yellow" />
        <stop offset="50%" stop-color="black" />
        <stop offset="60%" stop-color="red" />
      </linearGradient>
    </defs>
    
    <rect x="50" y="50" width="100" height="50" fill="url('#myGradient2')" />
  </svg>
</div>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
  • Thank you for helping me with my problem! It is a very good and relevant question you are asking. I wish I would have provided an answer in my OP already. What I am after is to translate SVG linear gradients into a simpler proprietary vector format that doesn't support userSpaceOnUse and also doesn't support transformations. Canvas won't be used, so that was perhaps a distraction in my OP. What I need is a function that takes four numbers and returns four numbers, a function that does what the toUserSpaceOnUse function in my OP does, but the other way. – user1283776 Jun 22 '20 at 19:18
  • Here is where I got the toUserSpaceOnUse function from: https://stackoverflow.com/a/62142147/1283776 – user1283776 Jun 22 '20 at 19:33
  • I've updated my answer. I believe my new function is correct. Note that this trick of modifying the gradient coords to simulate the objectBoundingBox transformation will only work for linear gradients. This wouldn't work for other types, such as radial gradients. – Paul LeBeau Jun 26 '20 at 17:35
  • Wow, I started looking at this at about the same time as this question was posted. This is fairly obvious as well, but a linearGradient element that also has a matrix function applied via gradientTransform would need to be converted. A use case I have is taking an arbitrary smaller SVG (with unique ids) and adding into a larger SVG. :-) – mradcliffe Jun 26 '20 at 18:18
  • 1
    There is no way to support arbitrary `gradientTransform` attributes this way (ie via coordinate manipulation). The target format would need to support gradient transforms also. – Paul LeBeau Jun 27 '20 at 05:05