html5-canvas 中的可变字体

Variable-Fonts in html5-canvas

我遇到了可变字体的问题,想知道是否有人有解决方案。我已经使用可变字体构建了这个海报生成器,您可以在其中操纵两个轴上的字体变化设置。这是一个活生生的例子http://automat.markjulienhahn.de

现在我正在尝试通过 html2canvas 下载结果。不幸的是,canvas-objects 似乎不支持可变字体,因此 canvas-object 只能显示字体的一种状态,fontVariationSettings 没有任何效果。

这就是我拉 canvas 元素的方式:

<script src="html2canvas.min.js"></script>    
  
<script>
    
var app = new Vue({
  el: '#app',
  methods: {
    saveCanvas(){
            html2canvas(document.querySelector("#capture")).then(
                canvas => {
                document.body.appendChild(canvas);
                var image = canvas.toDataURL("image/png").replace("image/png",  "image/octet-stream");
                console.log(image);  
                window.location.href=image;    
            });  
    }    
  }
})

</script>

这就是我操作可变字体的方式。

function randomizeState() {
    randomWeight = Math.floor(Math.random(1,100) * 100);
    randomWidth = Math.floor(Math.random(1,100) * 100);
    document.getElementById("element").style.fontVariationSettings = "\"frst\" " + randomWeight + ", \"scnd\" " + randomWidth;
    document.getElementById("state1").innerHTML = randomWeight + " " + randomWidth;
}

如有任何帮助,我将不胜感激!

不幸的是你是对的,我们目前不能直接在 canvas 中使用可变字体。所以这使得 html2canvas 的 canvas 渲染器无法正确渲染。

新版本的html2canvas附带了一个foreignObjectRenderer,它利用canvasAPI的能力绘制SVG图像,结合SVG的能力在 <foreignObject>.

中包含 HTML 个元素

这确实是我们必须在 canvas 上绘制可变字体的唯一当前解决方案,但是为此,字体需要嵌入到将在 [= 上绘制的 svg 文档中46=]。而且,html2canvas 不会为我们做这件事(即使我最近没有检查,我也不认为像 DOM2image 这样的其他解决方案也能做到这一点)。

所以我们必须自己做。

  • 首先我们需要获取字体文件 (woff2) 并将其编码为 data:// URL 以便它可以存在于独立的 svg 文件中。
  • 然后我们将使用我们元素的副本和它们所需的计算样式.
  • 构建<foreignObject>元素
  • 最后,我们将构建带有 <foreignObject><style> 的 svg 图像,从 data:// URL 声明我们的字体,并将其绘制在canvas.

(async () => {

  const svgNS = "http://www.w3.org/2000/svg";
  const svg = document.createElementNS( svgNS, "svg" );
  const font_data = await fetchAsDataURL( "https://fonts.gstatic.com/s/inter/v2/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2" );
  const style = document.createElementNS( svgNS, "style" );
  style.textContent = `@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 200 900;
    src: url(${ font_data }) format('woff2'); 
  }`;
  svg.append( style );
  
  const foreignObject = document.createElementNS( svgNS, "foreignObject" );
  foreignObject.setAttribute( "x", 0 );
  foreignObject.setAttribute( "y", 0 );

  const target = document.querySelector( ".target" );
  const clone = cloneWithStyles( target );
  foreignObject.append( clone );
  
  const { width, height } = target.getBoundingClientRect();
  foreignObject.setAttribute( "width", width );
  foreignObject.setAttribute( "height", height );
  svg.setAttribute( "width", width );
  svg.setAttribute( "height", height );
  
  svg.append( foreignObject );
  
  const svg_markup = new XMLSerializer().serializeToString( svg );
  const svg_file = new Blob( [ svg_markup ], { type: "image/svg+xml" } );
  
  const img = new Image();
  img.src = URL.createObjectURL( svg_file );
  await img.decode();
  URL.revokeObjectURL( img.src );
  
  const canvas = document.createElement( "canvas" );
  Object.assign( canvas, { width, height } );
  const ctx = canvas.getContext( "2d" );
  ctx.drawImage( img, 0, 0 );

  document.body.append( canvas );
  
})().catch( console.error );


function fetchAsDataURL( url ) {
  return fetch( url )
    .then( (resp) => resp.ok && resp.blob() )
    .then( (blob) => new Promise( (res) => {
        const reader = new FileReader();
        reader.onload = (evt) => res( reader.result );
        reader.readAsDataURL( blob );
      } )
    );
}
function cloneWithStyles( source ) {
  const clone = source.cloneNode( true );
  
  // to make the list of rules smaller we try to append the clone element in an iframe
  const iframe = document.createElement( "iframe" );
  document.body.append( iframe );
  // if we are in a sandboxed context it may be null
  if( iframe.contentDocument ) {
    iframe.contentDocument.body.append( clone );
  }
  
  const source_walker = document.createTreeWalker( source, NodeFilter.SHOW_ELEMENT, null );
  const clone_walker = document.createTreeWalker( clone, NodeFilter.SHOW_ELEMENT, null );
  let source_element = source_walker.currentNode;
  let clone_element = clone_walker.currentNode;
  while ( source_element ) {
  
    const source_styles = getComputedStyle( source_element );
    const clone_styles = getComputedStyle( clone_element );

    // we should be able to simply do [ ...source_styles.forEach( (key) => ...
    // but thanks to https://crbug.com/1073573
    // we have to filter all the snake keys from enumerable properties...
    const keys = (() => {
      // Start with a set to avoid duplicates
      const props = new Set();
      for( let prop in source_styles ) {
        // Undo camel case
        prop = prop.replace( /[A-Z]/g, (m) => "-" + m.toLowerCase() );
        // Fix vendor prefix
        prop = prop.replace( /^webkit-/, "-webkit-" );
        props.add( prop );
      }
      return props;
    })();
    for( let key of keys ) {
      if( clone_styles[ key ] !== source_styles[ key ] ) {
        clone_element.style.setProperty( key, source_styles[ key ] );
      }
    }

    source_element = source_walker.nextNode()
    clone_element = clone_walker.nextNode()
  
  }
  // clean up
  iframe.remove();

  return clone;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 200 900;
  src: url(https://fonts.gstatic.com/s/inter/v2/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2');
}

.t1 {
  font-family: 'Inter';
  font-variation-settings: 'wght' 200;
}
.t2 {
  font-family: 'Inter';
  font-variation-settings: 'wght' 900;
}

canvas {
  border: 1px solid;
}
<div class="target">
  <span class="t1">
    Hello
  </span>
  <span class="t2">
    World
  </span>
</div>