user1283776
user1283776

Reputation: 21784

Algorithm to convert userSpaceOnUse coordinates to objectBoundingBox coordinates?

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>

Upvotes: 4

Views: 633

Answers (1)

Paul LeBeau
Paul LeBeau

Reputation: 101888

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>

Upvotes: 6

Related Questions