如何选择字体大小以在 canvas2d 中制作字形纹理

How to choose a font size for making a glyph texture in canvas2d

我想使用 Canvas2D 为 WebGL 制作字形纹理。字形纹理是一种充满字母的纹理,可用于呈现文本。

我 运行 遇到的问题是我看不到,考虑到 Canvas API,如何在跨平台中做到这一点 "good"方法。我是什么意思?好吧,例如,如果我使用一些旧的 8 位字体,其中我知道每个字符都是 8x8 像素,那么我会知道如果我想说 32 个字形,我需要一个纹理,它是 32 个 8x8 单元的倍数。比如 256x8 或 128x16 或 64x32 等。我将每个字形的字符放在每个单元格中并完成。

不幸的是,我没有找到任何好的方法来使用 canvas2D api 计算字母的大小,而不需要大量的工作。

例如:假设我想要像字形一样的 8x16 EGA。好吧,让我们检查一下。我告诉浏览器我想要一个“8px monospace”字体。

var ctx = document.createElement("canvas").getContext("2d");
ctx.font = "8px monospace";
var dim = {
  minWidth: 100,
  maxWidth: 0,
};
for (var ii = 33; ii < 128; ++ii) {
  var letter = String.fromCharCode(ii);
  var t = ctx.measureText(letter);
  dim.minWidth = Math.min(t.width, dim.minWidth);
  dim.maxWidth = Math.max(t.width, dim.maxWidth);
}
console.log(dim);

它打印

// Object {minWidth: 0, maxWidth: 4.80078125}

这显然不是我想要的。在 AFAICT 之上,字体的大小取决于浏览器和平台。我什至不确定我是否使用自己的字体,如果这能保证为给定的字体大小规范呈现多大的字形。

一种方法是尝试不同的尺寸,测量它们的字符,然后选择最接近的匹配。这听起来效率很低。

另一个可能只是选择一个大小并渲染所有字符,然后只使用它们在 WebGL 中使用时缩放的大小。在这种情况下,尽管选择“8px monospace”会使字体变得非常难看,至少在 Chrome 中是这样。我可以迭代直到找到看起来不错的东西,但我不知道它在另一个浏览器或另一个平台上是否看起来不错。

是否有一些官方方法可以知道字体将呈现的大小,所以如果你想制作字形,你可以吗?或者是否有其他一些关于一般如何执行此操作的参考?就像非浏览器本机应用程序如何处理这个问题。

我想对于大多数本机应用程序,您只需让用户选择字体大小,然后在您 运行 超出 space 时裁剪或换行。但是对于一款游戏,您通常需要您的文本适合一些特定大小的 space,因此您需要选择一种适合 space 的字体,这似乎是问题所在。

顺便说一句:提出这个问题 this article about text in WebGL

我根本不建议为此使用 canvas2d(编辑:因为 canvas2d 在与字形相关的所有方面都很糟糕,不仅限于无法计算出字形的高度,它 不可避免地 导致大量工作。)。在我看来,无论是 SVG 还是 HTML,这取决于您的喜好,两者都非常擅长与字体相关的所有事情,并且 CSS 使得调整所有与字体相关的选项和指标变得非常容易。

我认为(但不能 100% 确定)所有主流浏览器都支持通过数据 uri 将 svg 光栅化为纹理。

let img = new Image();
img.src = `data:image/svg+xml,
    <svg xmlns="http://www.w3.org/2000/svg" width="128" height="128">
        <!-- some grid layout with inlined styles -->
    </svg>
`;
 img.onload = function ( loadedTexture) { ... } 

(如果你想使用 html 它显然必须是有效的 xhtml 嵌入到 <foreignObject> 标签中的 svg 中。我不知道那是怎么回事.)

编辑:添加 an example 基本上只使用 HTMLElement.prototype.getBoundingClientRect() 来测量字形并呈现任何字体,而不仅仅是等宽字体系列。一个潜在的陷阱:您需要 明确设置一种字体,它可能会向纹理呈现不同 字体。 TT

var img = new Image;

img.src = "data:image/svg+xml,"+glyphSource.outerHTML;

img.onload = function( ) {
    document.body.appendChild( img );
    var canvas = document.createElement("canvas");
    canvas.width = canvas.height = 128;
    
    document.body.appendChild( canvas );
    var container = glyphSource.children[0].children[0];
    
    

    var glyphs = [].slice.call( container.children );
    var cR = glyphs[0].getBoundingClientRect();
    var minY = cR.top;
    var minX = cR.left;
    var w = 128;
    var h = 128;
    
    const SIZE = 128;
    const DIM = .25;
    
    var glyphUV = glyphs.map( function( glyph ) {
        var d = glyph.getBoundingClientRect();
        //sampling upside down
       
        return [ 
            d.left,d.top,
            d.right,d.top,  
            d.left,d.bottom,
            d.left,d.bottom,
            d.right,d.top,
            d.right,d.bottom
        ];
    });
    
    const GLYPH_ENUM = {};
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890".split("")
    .forEach( function ( letter, index ) { GLYPH_ENUM[ letter ] = index } );
    console.log( "a",glyphs[ GLYPH_ENUM[ "m" ] ].getBoundingClientRect() );
    var posArr = [];
    var uvArr = [];
    var totalLength = 0;
    var SCALE = 2;
    function writeString( str ) {        
        var offX = 0;
        var offY = 0;
        str.split("").forEach( function( letter ) {
            var letterIndex = GLYPH_ENUM[ letter ];
            var r = glyphs[ letterIndex ].getBoundingClientRect();
            posArr.push(
                offX, offY,
                offX + r.width, offY,
                offX, offY + r.height,
                offX, offY + r.height,
                offX + r.width, offY,
                offX + r.width, offY + r.height
            );
            offX += r.width;
            totalLength++;
            uvArr.push.apply( uvArr, glyphUV[ letterIndex ] );
        });
    };
    writeString("Textrendering");

    window.gl = canvas.getContext("webgl");
    var program = gl.createProgram();
    var fs = gl.createShader( gl.FRAGMENT_SHADER );
    
    gl.shaderSource( fs, `
        precision mediump float;
        varying vec2 texCoord;
        uniform sampler2D glyphs;
        void main ( void ) {
            vec2 uv = gl_FragCoord.xy;
            gl_FragColor = texture2D( glyphs, texCoord );
        
        }
    `);
    gl.compileShader( fs );
    
    if (! gl.getShaderParameter( fs, gl.COMPILE_STATUS ) ) 
        return console.warn( gl.getShaderInfoLog( fs ) );
    
    var vs = gl.createShader( gl.VERTEX_SHADER );
    gl.shaderSource( vs, `
        attribute vec2 pos;
        attribute vec2 uv;
        varying vec2 texCoord;
        #define SCALE 2.5
        void main ( void ) {
            vec2 p = ( pos / vec2( ${w}, ${h} ) * SCALE - 1. ) * vec2( 1, -1);
            gl_Position = vec4( p, 0, 1 );
            texCoord = ( 
                uv - vec2( ${minX}, ${minY} ) 
            ) / vec2( ${w}, ${h} );
        }
    ` );
    gl.compileShader( vs );
    
    if (! gl.getShaderParameter( vs, gl.COMPILE_STATUS ) ) 
        return console.warn( gl.getShaderInfoLog( vs ) );
    
    gl.attachShader( program, vs );
    gl.attachShader( program, fs );
    gl.linkProgram( program );
    
    var tex0 = gl.createTexture();
    gl.bindTexture( gl.TEXTURE_2D, tex0 );

    gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img );
    console.log( img.width, img.height );
    gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST );  
    //gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST );  
    
    var pos = gl.createBuffer( );
    gl.bindBuffer( gl.ARRAY_BUFFER, pos );
    
    gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(
        posArr                  
    ), gl.STATIC_DRAW );
    gl.vertexAttribPointer( 0, 2, gl.FLOAT, 0, 0, 0 );
    gl.enableVertexAttribArray( 0 );
    var uv = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, uv );
    var a = 8;
    var b = 8+14;
    
    gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(
        uvArr
    ), gl.STATIC_DRAW );
    gl.vertexAttribPointer( 1, 2, gl.FLOAT, 0, 0, 0 );
     gl.enableVertexAttribArray( 1 );
    
    gl.clearColor( .5, .5, .5, 1 );
    gl.clear( gl.COLOR_BUFFER_BIT );
    gl.useProgram( program );
    gl.drawArrays( gl.TRIANGLES, 0, 6*totalLength );
   
    glyphSource.style.display = "none";
    img.style.display = "none";
}
<svg id="glyphSource" xmlns="http://www.w3.org/2000/svg" width="128" height="128" style="background:#CCC;">
    <foreignObject width="100%" height="100%" style="">
        <div xmlns= "http://www.w3.org/1999/xhtml" style="font:15px Helvetica;">
            <span>A</span>
            <span>B</span>
            <span>C</span>
            <span>D</span>
            <span>E</span>
            <span>F</span>
            <span>G</span>
            <span>H</span>
            <span>I</span>
            <span>J</span>
            <span>K</span>
            <span>L</span>
            <span>M</span>
            <span>N</span>
            <span>O</span>
            <span>P</span>
            <span>Q</span>
            <span>R</span>
            <span>S</span>
            <span>T</span>
            <span>U</span>
            <span>V</span>
            <span>W</span>
            <span>X</span>
            <span>Y</span>
            <span>Z</span>
            <span>a</span>
            <span>b</span>
            <span>c</span>
            <span>d</span>
            <span>e</span>
            <span>f</span>
            <span>g</span>
            <span>h</span>
            <span>i</span>
            <span>j</span>
            <span>k</span>
            <span>l</span>
            <span>m</span>
            <span>n</span>
            <span>o</span>
            <span>p</span>
            <span>q</span>
            <span>r</span>
            <span>s</span>
            <span>t</span>
            <span>u</span>
            <span>v</span>
            <span>w</span>
            <span>x</span>
            <span>y</span>
            <span>z</span>
            <span>1</span>
            <span>2</span>
            <span>3</span>
            <span>4</span>
            <span>5</span>
            <span>6</span>
            <span>7</span>
            <span>8</span>
            <span>9</span>
            <span>0</span>
        </div>
    </foreignObject>
</svg>