HTML5画布中的可变字体

3

我遇到了一个与变量字体相关的问题,想知道是否有人能提供解决方案。我制作了一个海报生成器,使用变量字体,可以在两个轴上操纵字体变量设置。这是一个实例 http://automat.markjulienhahn.de

现在我正在尝试通过html2canvas下载结果。不幸的是,看起来画布对象不支持变量字体,所以画布对象只能显示字体的一种状态,而fontVariationSettings没有任何效果。

以下是获取画布元素的代码:

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

我很感激您的帮助!
1个回答

3

很不幸,您是对的,目前我们无法直接在画布中使用可变字体。因此,这使得html2canvas的画布渲染器无法正确呈现它。

新版的html2canvas带有一个foreignObjectRenderer,它利用画布API绘制SVG图像的能力,结合SVG在<foreignObject>中包含HTML元素的能力。

目前,这确实是我们在画布上绘制可变字体的唯一解决方案,但需要将字体嵌入到将在画布上绘制的svg文档中。而这,html2canvas并没有为我们做到(虽然我最近没有检查过,但我认为其他解决方案如DOM2image也没有做到这一点)。

所以我们必须自己来做。

  • 首先,我们需要获取字体文件(woff2)并将其编码为data:// URL,以便它可以存在于独立的svg文件中。
  • 然后,我们将使用元素及其所需的计算样式构建<foreignObject>元素。
  • 最后,我们将使用<foreignObject><style>来声明来自data:// URL的字体,并将其绘制在画布上。

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


非常好用,谢谢!以前字体周围有一道笔画,但是这种方法会丢失。我该如何将其添加到字体中?我尝试将其添加到style变量中,但它没有出现。有什么想法吗? - Mark Julien Hahn
是的,CSS定位很好。我的意思是在字母周围画一个简单的轮廓,我可以通过CSS中的“-webkit-text-stroke: 1px black;”来实现。不幸的是,在SVG中这并不起作用。 - Mark Julien Hahn
哦...奇怪,那个对我来说确实有效:https://jsfiddle.net/5yoexz4f/ - Kaiido
在Safari中似乎存在问题。在Chrome中它运行良好。 - Mark Julien Hahn
你能提供一个简单的解决方案,将SVG文件转换为可下载的JPG文件吗? - Mark Julien Hahn
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接