【问题标题】:SVG and Canvas read SVG data differentlySVG 和 Canvas 读取 SVG 数据的方式不同
【发布时间】:2017-11-17 09:33:54
【问题描述】:

我正在尝试编写一些 JS,它使用 Canvas 来模拟 SVG 如何在图像上绘制形状,但我似乎无法让它工作。

我有一些 SVG 数据可以在脸上画出嘴巴。

<svg id="svgTag" style="background: url('https://image.ibb.co/h9CWiR/odd_size.png') no-repeat " xmlns="http://www.w3.org/2000/svg" version="1.0" width="379.000000" height="540.000000" viewBox="0 0 379.000000 540.000000" preserveAspectRatio="xMidYMid meet">
    <g transform="translate(0.000000,540.0000) scale(1.000000,-1.000000)" fill="#000000" stroke="none">
        <path d="M172 147
               c-21 -1 -69 -8 -74 -10
               l-2 -1 0 -32 0 -33 3 -4
               c9 -14 31 -36 44 -45
               12 -9 16 -11 27 -13
               2 -1 8 -2 12 -2
               9 -2 33 -3 34 -1
               0 1 1 1 2 1
               2 0 6 1 9 2
               16 5 29 14 47 32
               l9 8 0 44
               c0 33 0 43 -1 44 -4 3 -31 8 -49 10 -14 1 -44 1 -61 0
               z"/>
    </g>
</svg>

我在这里有一个小提琴:https://jsfiddle.net/nhoughto5/5dhn1nm1/

我编写了一些 javascript,它从 SVG 元素中获取路径数据,对其进行解析,然后使用 Canvas 方法绘制同一张嘴。我已经实现了一个递归解决方案来逐步执行每个命令并以完全相同的方式绘制它。

 const commandString = /[\-0-9,. ]+/ig;
  const commandPattern = /[A-z]+/ig;
  const stringSplit = /[, ]+/ig;
  const shiftX = 0, shiftY = 0;

  function cleanArray(array) {
    const tmpArray = [];
    for (let i = 0; i < array.length; i++) {
      if (array[i].length > 0) {
        tmpArray.push(parseInt(array[i]));
      }
    }
    return tmpArray;
  }
  function move(array, context) {
    let x = array[0];
    let y = array[1];
    context.moveTo(x + shiftX, y + shiftY);
    //context.fill();
  }

  function quadraticTo(array, context) {
    let x = array[0];
    let y = array[1];
    let x1 = array[2];
    let y1 = array[3];
    context.quadraticCurveTo(x + shiftX, y + shiftY, x1 + shiftX, y1 + shiftY);
    context.fill();
    if (array.length > 4) {
      quadraticTo(array.splice(4, array.length), context);
    }
  }

  function lineTo(array, context) {
    let x = array[0];
    let y = array[1];
    context.lineTo(x + shiftX, y + shiftY);
    context.fill();
    if (array.length > 2) {
      lineTo(array.splice(2, array.length), context);
    }
  }

  function curveTo(array, context) {
    let x = array[0];
    let y = array[1];
    let x1 = array[2];
    let y1 = array[3];
    let x2 = array[4];
    let y2 = array[5];
    context.bezierCurveTo(x + shiftX, y + shiftY, x1 + shiftX, y1 + shiftY, x2 + shiftX, y2 + shiftY);
    context.fill();
    if (array.length > 6) {
      this._curveTo(array.splice(6, array.length), context);
    }
  }
  function draw(path, context){
    var commands = path.split(commandPattern);
    var commandArray = path.split(commandString);

    for (let i = 0, len = commandArray.length; i < len; i++) {
      let action = commandArray[i].toUpperCase();
      if (typeof commands[i] === 'undefined') break;

      const array = cleanArray(commands[i].split(stringSplit));
      switch (action) {
        case "M":
          move(array, context);
          break;
        case "C":
          curveTo(array, context);
          break;
        case "Q":
          quadraticTo(array, context);
          break;
        case "L":
          lineTo(array, context);
          break;
        case "Z":
          context.closePath();
          break;
        default:
      }
    }
  }
  function makeCanvas(canvas, ctx, img){
    var svg = document.getElementById("svgTag");
    var groups = svg.getElementsByTagName("g");
    var path = groups[0].childNodes[1].getAttribute("d");
    ctx.drawImage(img, 10, 10);
    ctx.fillStyle = "black";
    console.log(path);
    draw(path, ctx);
  }
  function imageCanvas() {
    var c = document.getElementById("myCanvas");
    var ctx = c.getContext("2d");
    var img = document.createElement("IMG");
    img.addEventListener('load', function() {
      makeCanvas(c, ctx, img);
    }.bind(this), false);
    img.setAttribute("src", "https://image.ibb.co/h9CWiR/odd_size.png");

  }

  window.onload = imageCanvas();

再次,我在这里创建了一个小提琴演示:https://jsfiddle.net/nhoughto5/whwxtxcp/1/

问题是它似乎完全错误。起初我认为这可能是 SVG 组中的 translate 值的问题。我在 JS 的顶部添加了一个常量,以便轻松翻译它。如果您将翻译常量修改为:const translateX = 100, translateY = 300; 它所绘制的内容将移至中间,您会发现它非常错误。

我已经逐步完成了我的解决方案,并检查了数据是否以正确的顺序拼接并绘制到画布上。画布是否与我不知道的 SVG 有什么不同?不要自吹自擂,但我相当有信心我的实现是正确的,但我缺少一些东西。

【问题讨论】:

  • 乍一看,您似乎忽略了路径中绝对命令和相对命令之间的区别。另外我认为你在解析真实世界的 SVG 时会有很多惊喜,语法比看起来要糟糕得多。
  • @jcaron,我认为你是对的。反正有没有说明路径是如何定义的?我在 SVG 本身中看不到任何明显的东西。
  • @jcaron。 Nvm,回答了我自己的问题^。文档指出:“所有命令也有两种变体。大写字母指定页面上的绝对坐标,小写字母指定相对坐标(例如,从最后一点向上移动 10 像素,向左移动 7 像素)。”

标签: javascript canvas svg


【解决方案1】:

设置 Path2D 的相对原点

您的问题是您错误地设置了相对位置。您还需要设置正确的变换。

变换

SVG 转换为translate(0.000000,540.0000) scale(1.000000,-1.000000)

相当于

ctx.setTransform(1, 0, 0, -1, 0, 540);
//               |         |  |  |
//              (x scale)  | (x  y  translate)
//                        (y scale) 

d路径

对于命令“M x1 y1 c x2 y2 x3 y3 x4 y4 l x5 y5”,其中 x,y 表示坐标对。

等效的画布命令是

var ox = 0, oy = 0; // the relative origin
ox = X1; oy = Y1;
ctx.moveTo(ox, oy);
ctx.bezierCurveTo(ox + x2, oy + y2, ox + x3, oy + y3, ox = ox + x4, oy = oy + y4)
//                                                    ^^ new origin ^^ new origin
ctx.lineTo(ox = ox + x5, oy = ox + y5);
//         ^^ new origin ^^ new origin

注意,每个段的最后一个点用于设置新的相对原点

下面的代码是对你的一些 sn-p 的修改,它可以正确解析和执行路径命令。

// Only partial solution
// implement a subset of the 2D path string commands
const canvasPath = (()=>{
    var ctx, ox = 0, oy = 0;
    const [x, y] = [(p) => ox + p.shift(), (p) => oy + p.shift()];
    const X = (p) => p.shift(), Y = X;
    return {
        set context(ctx_) { ctx = ctx_ },
        c(p) { while(p.length > 0) { ctx.bezierCurveTo(x(p), y(p), x(p), y(p), ox = x(p), oy = y(p)) } },     
        q(p) { while(p.length > 0) { ctx.quadraticCurveTo(x(p), y(p), ox = x(p), oy = y(p)) } },        
        l(p) { while(p.length > 0) { ctx.lineTo(ox = x(p), oy = y(p)) } },
        z()  { ctx.closePath() },
        M(p) { ctx.moveTo(ox = X(p), oy = Y(p)) },
    };
})();


// This may throw for some paths as it cobers only some path commands 
function parsePath(path) {  
    var subPath, paths = [];
    path.replace(/([Mzlqc])|([\-0-9.]+)/g, s => {        
        if ("Mzlqc".indexOf(s) > -1) { paths.push(subPath = {type : s , points : []}) }
        else { subPath.points.push(Number(s)) }
        return s;
    });
    return paths;
}

// parses a path string and then adds that path to the canvas via canvasPath
function draw(path) {
    const subPaths = parsePath(path);
    for (const {type, points} of subPaths) { canvasPath[type](points) }
}

// Taken from OP snippet and modified to use above functions.
function makeCanvas(canvas, ctx, img) {
    var svg = document.getElementById("svgTag");
    var groups = svg.getElementsByTagName("g");
    var path = groups[0].childNodes[1].getAttribute("d");
    ctx.drawImage(img, 0, 0); // NOTE image moved to 0,0

    // from SVG "translate(0.000000,540.0000) scale(1.000000,-1.000000)
    ctx.setTransform(1, 0, 0, -1, 0, 540);    
    canvasPath.context = ctx;
    ctx.beginPath();
    draw(path);
    ctx.fill()
}

使用Path2D

但这是一项艰巨的工作,当您可以使用 Path2D 对象为您完成工作时,为什么还要这样做。

Path2D 将接受 SVG 路径

const path = new Path2D(`M172 147
    c-21 -1 -69 -8 -74 -10
    l-2 -1 0 -32 0 -33 3 -4
    c9 -14 31 -36 44 -45 12 -9 16 -11 27 -13  2 -1 8 -2 12 -2  9 -2 33 -3 34 -1 0 1 
    1 1 2 1 2 0 6 1 9 2 16 5 29 14 47 32
    l9 8 0 44
    c0 33 0 43 -1 44 -4 3 -31 8 -49 10 -14 1 -44 1 -61 0 z`
);

您可以使用填充和描边命令来渲染路径

ctx.fill(path);
ctx.stroke(path);

因此您的代码被简化为。

  function makeCanvas(canvas, ctx, img){
    var svg = document.getElementById("svgTag");
    var groups = svg.getElementsByTagName("g");
    var path = new Path2D(groups[0].childNodes[1].getAttribute("d"));
    ctx.drawImage(img, 0, 0); // NOTE image moved to 0,0 was 10,10
    // from SVG "translate(0.000000,540.0000) scale(1.000000,-1.000000)
    ctx.setTransform(1,0,0,-1,0,540);    
    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.fill(path);
  }

MDN 已过时Path2D 受 Chrome 和 FireFox 支持。虽然您应该检查浏览器的兼容性,因为它是最近添加的。

【讨论】:

  • 还有一些适用于不支持 Path2d 的浏览器的 polyfill。例如 github.com/google/canvas-5-polyfill 包含一个。而且 Path2d 或 polyfill 也将允许 S Bezier Curve command 如果我没记错的话,因为 CanvasContext2D API 中没有内置等效项。
  • 哦,但你为什么说“MDN 已过时Path2D Chrome 和 FireFox 支持。”?它们可能已经过时了,但他们确实表示 Chrome 和 FF 以及 Edge 和 Safari10+ 都支持它。
  • @Kaiido 页面 developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D 最后更新于 2017 年 9 月 26 日显示对 Chrome、Opera 和 Safari 的未知支持,并且不包括 Edge。
  • 啊,我明白了,构造函数的页面...奇怪的是,当我们看到您在答案中链接到的 API's page 似乎是最新的,并且“基本支持 " 显然包括构造函数,因为它是访问此 API 的唯一方法。
猜你喜欢
  • 2014-04-13
  • 2013-08-18
  • 1970-01-01
  • 2020-02-06
  • 1970-01-01
  • 2012-03-16
  • 1970-01-01
  • 2013-03-31
  • 1970-01-01
相关资源
最近更新 更多