Алгоритм преобразования координат userSpaceOnUse в координаты objectBoundingBox? - PullRequest
3 голосов
/ 17 июня 2020

Эти два SVG имеют линейные градиенты, которые выражаются в разных системах координат, но визуализируют одно и то же изображение. Я хотел бы иметь возможность конвертировать между этими системами координат. Я знаю, как преобразовать из objectBoundingBox в userSpaceOnUse, но не в другое направление.

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

В приведенном ниже примере toUserSpaceOnUse преобразует координаты градиента SVG из objectBoundingBox в userSpaceOnUse. Как будет выглядеть функция, которая делает противоположное, преобразует из userSpaceOnUse в координаты objectBoundingBox, 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>

1 Ответ

3 голосов
/ 22 июня 2020

Думаю, я понимаю, что вы сейчас пытались сделать. Вы предполагали, что координаты градиента всегда были 0% 0% 100% 100%, а затем пытались вычислить абсолютные координаты градиента, которые имитируют «растяжение», создаваемое преобразованием objectBoundingBox.

Есть гораздо более простой способ сделать это. Нет необходимости в сложной функции расчета. См. Ниже.

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>

Это помогает?

Почему вам нужно go другим способом (от userSpaceOnUse к objectBoundingBox)? Ваша конечная цель - выполнить рендеринг на HTML Canvas или что-то еще? Если я смогу понять, что вам нужно, я смогу лучше ответить на ваш вопрос.

Обновление

Вот обратная функция, которую вы искали.

Я начал с изменения вашей исходной функции, чтобы поддерживать координаты objectBoundingBox, отличные от (0% 0% 100% 100%).

Тогда для обратной функции это просто вопрос обратного действия операций исходная функция.

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