【问题标题】:How to draw xlink:href to canvas如何将 xlink:href 绘制到画布上
【发布时间】:2018-11-19 09:54:19
【问题描述】:

我有多个要渲染的 SVG 形状。

我目前正在为每个 svg 分别创建一个 Image 对象,这会导致许多不必要的 HTTP 请求。

const imageEl = new Image();
imageEl.src = image.src;
// And then
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);

我想使用一个包含我所有 SVG 的 sprite 文件并将它们绘制到画布上,就像在 HTML 中使用 <use xlink:href="SOME_URL"></use> 一样

我该怎么做?

【问题讨论】:

    标签: javascript svg canvas


    【解决方案1】:

    几点提醒:

    • 从你的问题中不清楚你是如何管理你的资产的,所以我添加这个作为提醒,但请注意它是这个答案最重要的一点: 你应该加载您的所有资产,只需一次,并且在您开始使用其中任何一项之前。
      这意味着,new Image() 调用应该在应用的初始化步骤中进行,drawImage() 应该在应用的绘图步骤中进行,它本身在动画循环中调用。

      // in 'loadAssets()' called from 'init()' only once  per 'uri'
      const img = new Image();
      img.onload = thisLoaded;
      img.src = uri;
      
      // in 'draw()' called from 'anim()'
      ctx.drawImage(img, ...
      
    • 您的问题归结为能够从 元素加载这些引用。这就是 Canvas2d API 作为源代码所需要的,而这实际上是这里最大的罪魁祸首。 (drawImage 也接受 svg 的 但我认为这里没有任何相关性)

    • 大多数 svg 元素本身没有意义。我们用来定义它们的所有坐标都需要相对于它们的 viewPort 父级的 viewBox。所以几乎没有一个元素可以单独绘制。如果 svg 元素能够“渲染”单个元素,那是因为这个 <use> 元素本身包含在一个 元素中,定义 viewBox

    • 元素加载的图像不能执行对外部资源的任何请求。
      这意味着动态生成多个仅由 <svg><use href="https://foo.bar#baz"></svg> 组成的简单 svg 文档在这里对我们没有帮助,#baz 应该是文档本身的一部分。


    既然这一切都已经弄清楚了,我们可以想出一些解决方案:

    在下面的例子中,svg image used 由两个元素组成,一个 id 为 rect,另一个 id 为 circle

    第一个可能是最明显的,是构建自己的系统。
    您可以先获取您的 spritesheet svg,从 js 中解析,然后通过仅选择您想要的元素来生成您需要绘制的 svg。

    但这种方法最大的问题是 svg 的链接可能非常复杂,检索所有目标元素并不是一件容易的事。

    所以在这里,我只展示一个基本实现,我们只能定位直接元素(没有 导入,没有 url(#...) em> 等)。如果您真的需要完整的东西,请告诉我我前段时间确实写过一些东西...

    function loadSvgDoc(uri) {
      return fetch(uri)
      .then(r => r.text())
      .then(markup => (new DOMParser())
          .parseFromString(markup, 'image/svg+xml')
      );
    }
    
    function loadFakeUse(doc, id) {
      const root = doc.documentElement;
      const node = doc.getElementById(id);
      if(!root || !node)
        return Promise.reject('invalid params');
      const clone =  root.cloneNode();
      clone.append(node);
      const markup = (new XMLSerializer()).serializeToString(clone);
      return new Promise((res, rej) => {
        const img = new Image();
        img.onload = e => res(img);
        img.onerror = rej;
        img.src = 'data:image/svg+xml,' + encodeURIComponent(markup);
      });
    }
    
    loadSvgDoc('https://dl.dropboxusercontent.com/s/s7aynuqx6pe1we8/fake_use.svg')
      .then(doc => 
        Promise.all([
          loadFakeUse(doc, 'rect'),
          loadFakeUse(doc, 'circle')
        ])
      )
      .then(images => {
        const ctx = canvas.getContext('2d');
        images.forEach((img, i) => 
          ctx.drawImage(img, i * 120, 0)
        );
      })
      .catch(console.error);
      
    <canvas id="canvas" width="250" height="200"></canvas>

    另一种方法是一种 CSS hack,我首先从 Lea Verou 中了解到。
    CSS 有一个:target 选择器,它允许仅对文档哈希指向的元素设置样式。

    这意味着您可以根据 url 的 #id 中的目标元素设置一些规则。

    例如,我的 svg 有这些简单的规则:

     rect, circle { /* always hide */
       visibility: hidden;
     }
     :target { /* show only the targeted element */
       visibility: visible;
     }
    

    现在您只需在您的页面上做的就是

    function loadImage(url) {
      return new Promise((res, rej) => {
        const img = new Image();
        img.onload = e => res(img);
        img.onerror = rej;
        img.src = url;
      });
    }
    
    function loadImages(urls) {
      return Promise.all(urls.map(loadImage));
    }
    
    const base = 'https://dl.dropboxusercontent.com/s/s7aynuqx6pe1we8/fake_use.svg';
    
    loadImages([
        base + '#rect',
        base + '#circle'
      ])
      .then(images => {
        const ctx = canvas.getContext('2d');
        images.forEach((img, i) =>
          ctx.drawImage(img, i * 120, 0)
        );
      })
      .catch(console.error);
    <canvas id="canvas" width="250" height="200"></canvas>

    【讨论】:

      猜你喜欢
      • 2020-10-14
      • 1970-01-01
      • 2021-11-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-16
      • 2014-05-24
      • 2011-05-30
      相关资源
      最近更新 更多