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>
我遇到了可变字体的问题,想知道是否有人有解决方案。我已经使用可变字体构建了这个海报生成器,您可以在其中操纵两个轴上的字体变化设置。这是一个活生生的例子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>
.
这确实是我们必须在 canvas 上绘制可变字体的唯一当前解决方案,但是为此,字体需要嵌入到将在 [= 上绘制的 svg 文档中46=]。而且,html2canvas 不会为我们做这件事(即使我最近没有检查,我也不认为像 DOM2image 这样的其他解决方案也能做到这一点)。
所以我们必须自己做。
- 首先我们需要获取字体文件 (woff2) 并将其编码为
data://
URL 以便它可以存在于独立的 svg 文件中。 - 然后我们将使用我们元素的副本和它们所需的计算样式. 构建
- 最后,我们将构建带有
<foreignObject>
和<style>
的 svg 图像,从data://
URL 声明我们的字体,并将其绘制在canvas.
<foreignObject>
元素
(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>