使不同的字体显示为相同的实际大小?

时间:2017-08-07 14:23:04

标签: css font-size

当我在网页中使用相同字体大小的两种不同字体时,它们通常以不同的实际尺寸显示:

Text in two different fonts

此示例使用两个相同字体大小的Google字体,Gentium和Metamorphous,指定为20px。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <link id="Gentium Book Basic" rel="stylesheet" type="text/css" 
        href="http://fonts.googleapis.com/css?family=Gentium Book Basic" 
        media="all">
  <link id="Metamorphous" rel="stylesheet" type="text/css" 
        href="http://fonts.googleapis.com/css?family=Metamorphous" media="all">
  </head>
<body style="font-size: 20px">
  <span style="font-family: Gentium Book Basic">Test Text Length (Gentium)</span>
 <br>
 <span style="font-family: Metamorphous">Test Text Length (Metamorphous) </span>
</body>
</html>

可以找到此示例的JSBin here

我对以px这样的绝对长度指定font-size的理解是,字体将被缩放以匹配该长度。我的期望是相同字体大小的两种不同字体将具有匹配的高度或匹配长度(我理解字体的宽高比可能不同)。但它似乎不会出现在这里的情况。有没有什么方法可以使两个任意字体显示在相同的高度或相同的长度,而无需手动计算和应用校正?

编辑:显示以相同字体大小显示的两种字体的上升距离下移的示例。

enter image description here

显然,这两种字体的两个距离与显示的不同。

编辑:显示两种字体中带和不带重音的字母的示例:

enter image description here

同样,很明显这些字母的大小不同。

编辑:继续this article中描述的内容,问题是font-size控制字体em值的显示大小。但是em值是任意的(它不必与字体中的任何内容相对应,特别是不一定是小写字母的高度&m;#);并且不包括ascenders和下降,可以是任何大小(例如从上面的文章中取得):

enter image description here

所以结果是&#34; 100px&#34;字体可以是任何有效的大小。上述文章的作者计算了当时Google Web Fonts的有效大小范围为0.618到3.378。

由于字体指标(例如em大小,大写高度,上升和下降值)未在CSS中公开,因此在CSS中似乎没有任何方式可以使两个任意字体相同有效大小。对于任何特定字体,您可以使用字体编辑器查找字体度量值,并使用这些数字根据需要缩放字体。对于任意字体,选项是显示一些文本并使用测量的边界框来确定有效大小并计算适当的缩放因子。

感谢所有为解释此事做出贡献的人!

4 个答案:

答案 0 :(得分:1)

你应该使用rem之类的东西然后px :)因为rem是一个相对测量单位而px是绝对的。但是字体总是有不同的大小,因此不可能实现你想要达到的目标。

答案 1 :(得分:1)

font-size视为单个字符本身的实际大小,而不是包含每个字符的块的大小,就像排版字母一样:

typeset letters

块的大小在CSS中定义(使用px,pts,ems等),但这些块中字符的实际大小可能因使用的字体而异。

  

字体任何给定部分的实际物理高度取决于用户定义的DPI设置,当前元素字体大小以及所使用的特定字体。

     

https://en.wikipedia.org/wiki/Em_(typography)#CSS

您可以使用font-size-adjust属性来帮助改变其中一种字体,使其更接近另一种:https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust尽管它的支持目前仅限于Firefox:http://caniuse.com/#feat=font-size-adjust

答案 2 :(得分:1)

字体大小是上升符号的字形大小,例如字母“h”的字母大小,下降到字母的下方,例如字母“#”; 。如果您将字体大小设置为20px,则字母顶部的长度为&#39; h&#39;在信的底部&#39; g&#39;将是20px。有些字母有端子或马刺,字母的末端可能会在某些字母上延伸px或两个字母。

enter image description here

在您的示例中,两种字体之间存在px差异。 Metamorphous字体在Gentium没有的某些字母上面有一个标记,这就是高差的原因。

You can read more here.

编辑:点击这里&#34; caron&#34;在C上面比较右边的两个Gentium字母。

enter image description here

答案 3 :(得分:1)

我花了很多时间在 StackOverflow 中寻找类似情况的答案,但最终没有找到任何完美的东西。我最终做的是测量两种字体,然后调整第二种字体的上边距和比例以匹配第一种。 (通过使用 scale 而不是改变字体大小,它允许我们在调整大小后不需要重新计算文本度量)

我为子孙后代准备了几支笔。这是第二个,它处理字体大小的规范化和两种字体之间的对齐:https://codepen.io/zacholas/pen/oNBPWga

第一个,只处理测量,是:https://codepen.io/zacholas/pen/ExZwJjx


我想我必须粘贴一些代码才能链接到codepen,所以这里是所有的比较代码:

HTML:

<h1>Welcome</h1>
<p>
  <strong>What's all this about?</strong><br>
  I've been working on the new version of the <a href="https://app.mason.ai/" target="_blank">Mason image editor app</a>, and in it, I need to compare multiple fonts to replace them with each other to have the final layout not look crappy due to spacing differences between fonts. (To allow users to customize fonts in templates and have them still look nice)
</p>
<p>The <a href="https://codepen.io/zacholas/pen/ExZwJjx" target="_blank">first pen</a> focused on getting all the measurements and ratios necessary.</p>
<p>This pen encompasses the second part, which is comparison and normalization of different fonts against a default.</p>
<p>
  <strong>How it works</strong><br>
  First we get the metrics for the two fonts and compare their top/bottom spacing. We then normalize them to align in a purdy vertically-centered way. And then we scale down the second font's container so that it matches the size of the first.
</p>
<p>Enjoy!</p>
<p><em><a href="https://zachswinehart.com" target="_blank">- Zach</a></em></p>

<hr>
<h3>Demo</h3>
<p>If all of my code is working correctly, the text in the "new font adjusted" box should look all purdy and be vertically and horizontally centered.</p>
<p><strong>NOTE:</strong><em> You'll need to make a change in the dropdown before the text in the "new font adjusted" box actually gets adjusted.</em></p>
<label for="font-picker">Choose a font to swap:</label>
<select id="font-picker" disabled>
  <option value=""> — Template Default — </option>
</select>
<div >
  <div id="image-box" class="flex-row">
    <div>
      <h6>Original:</h6>
      <div class="reference-box">
        <div class="text-background">
          <div class="text-container text-utc-lander" id="original-text">
            Hxy
          </div>
        </div>
      </div>
    </div>
    
    <div>
      <h6>New font unadjusted:</h6>
      <div class="reference-box">
        <div class="text-background">
          <div class="text-container text-utc-lander" id="unadjusted-text">
            Hxy
          </div>
        </div>
      </div>
    </div>
    
    <div>
      <h6>New font adjusted:</h6>
      <div id="modified" class="reference-box">
        <div class="text-background">
          <div class="scaler" id="adjusted-text-scaler">
            <div class="text-container text-utc-lander" id="adjusted-text">
              Hxy
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<hr>

<h2>Canvases used for calculating</h2>

<div id="sample-output-container">
</div>

SCSS:

@import 'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css';

// The font size in the demo boxes
$test-font-size: 200px!default;
// $test-font-size: 100px;

body {
  background: #eee;
  padding: 10px;
  font-family: Arial;
}

hr {
  margin: 40px 0;
}

h6 {
  font-size: 17px;
  margin: 12px 0 5px 0;
}

//* In production, you should probably use code like this to position canvases off-screen:
// .test-canvas {
//   position: fixed;
//   top: -99999px;
//   left: -99999px;
//   display:none;
// }

.text-utc-lander { font-family: 'UTCLander-Regular'; }
.text-ar-bonnie { font-family: 'ARBONNIE'; }
.text-adam-cg { font-family: 'ADAMCGPRO'; }
.text-abolition { font-family: 'Abolition-Regular'; }
.text-avenir { font-family: 'AvenirNextLTPro-BoldItalic'; }
.text-agency { font-family: 'AgencyFB-Reg'; }

/* Testing a real life example with absolute CSS done to make a F-ed up font
   like UTC lander look good, which we'll then need to modify positioning and
   sizing for in order for it to look good with normal fonts */
.flex-row {
  display: flex;
  justify-content: space-between;
}

#image-box {
  .reference-box {
    background: url('https://mason-app-staging.herokuapp.com/images/sports_stadium_generic.jpg');
    background-size: cover;
    position: relative;
    width: $test-font-size * 2;
    height: $test-font-size * 1.2;
    
    &:before, &:after {
      content: '';
      left: $test-font-size * .1;
      right: $test-font-size * .1;
      position: absolute;
      height: 1px;
      background: rgba(0,0,0,0.1);
      z-index: 5;
    }
    
    &:before {
      top: $test-font-size * 0.245;
    }
    
    &:after {
      bottom: $test-font-size * 0.245;
    }
    
    .text-background {
      position: absolute;
      left: ($test-font-size * 0.1);
      top: ($test-font-size * 0.1);
      width: ($test-font-size * 1.8);
      height: ($test-font-size * 1);
      background:#39b510;
      color: #fff;
      text-transform: uppercase;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    .text-container {
      margin-top: -10px; // Will be overwritten anyway
      text-align: center;
      font-size: $test-font-size;
      line-height: 1;
    }
  }
}

#comparison-output {
  background: #fff;
  padding: 20px;
  margin-top: 40px;
  flex: 1;
}


//* Debug output from the first example
#sample-output-container {
  // * {
  //   line-height: 1;
  // }
  
  > div {
    width: 700px;
    background: #CCC;
    margin-bottom: 20px;
    position: relative;
    height: 200px;
    
    > .text-container {
      background: #fff;
      position: absolute;
      display: flex;
      height: 150px;
      left: 25px;
      width: 300px;
      top: 25px;
      align-items: center;
      justify-content: center;
      
      > span {
        background: #edc79e;
      }
    }
    
    > .info-box {
      font-size: 12px;
      font-family: Arial;
      background: #fff;
      position: absolute;
      width: 300px;
      top: 25px;
      right: 25px;
      padding: 10px;
    }
  }
}


/*
Webfonts
- Code from here down is just base 64'd webfonts.
- All are in "normal" font weight
- Families available:
   - 'ARBONNIE';
   - 'ADAMCGPRO';
   - 'Abolition-Regular';
   - 'AgencyFB-Reg';
   - 'AvenirNextLTPro-BoldItalic';
   - 'UTCLander-Regular';
*/

/* ***** SKIPPING BASE-64'D FONTS FOR STACKOVERFLOW */

JS:

import FontFaceObserver from "https://cdn.skypack.dev/fontfaceobserver@2.1.0";
// var FontFaceObserver = require('fontfaceobserver');
const TYPE_DEFAULT_FONT = 'defaultFont';
const TYPE_CURRENT_FONT = 'currentFont';

// debug output canvases
const removeCalculationCanvases = false;

const allAvailableFonts = [
    { label: 'AR Bonnie', value: 'ARBONNIE' },
    { label: 'Adam CG Pro', value: 'ADAMCGPRO' },
    { label: 'Abolition Regular', value: 'Abolition-Regular' },
    { label: 'Avenir Next LT Pro Bold Italic', value: 'AvenirNextLTPro-BoldItalic' },
    { label: 'Agency FB', value: 'AgencyFB-Reg' },
    { label: 'UTC Lander', value: 'UTCLander-Regular' },
]

const INITIAL_STATE = {
    [TYPE_DEFAULT_FONT]: {
        label: null,
        fontFamily: null,
        fontSize: null,
        metrics: {},
    },
    [TYPE_CURRENT_FONT]: {
        label: null,
        fontFamily: null,
        fontSize: null,
        metrics: {},
        postAdjustmentMetrics: {}
    }
}

let state = {
    ...INITIAL_STATE
}

const _roundToTwo = num => {
    return +(Math.round(Number(num) + "e+2")  + "e-2");
}

const _roundToFive = num => {
    return +(Math.round(Number(num) + "e+5")  + "e-5");
}

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}


const getTextMetrics = async(fontFamily, fontSize, testtext = 'Sixty Handgloves ABC') => {
    //* For now we'll just keep the test text hard-coded but maybe we'll pass in the element value at some point. (However, being that the text will be editable I don't think that's wise)
    testtext = 'Hxy';
  
    const fontSizePx = fontSize.split('px')[0];

    //* Generate a hash from the font name for the canvas ID
    const canvasId = Math.abs(fontFamily.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0));
    
    
    console.log('waiting for font to load')
    var font = new FontFaceObserver(fontFamily);
    await font.load();
    console.log('font loaded');

    //* Initialize the test canvas so that we can measure stuff
    const testCanvasWidth = 400;
    const testCanvasHeight = 200;
    const testCanvasPadding = 10;
    // const canvasDrawingTextFontSize = 1000;
    const canvasDrawingTextFontSize = fontSizePx;
    const testCanvas = document.createElement('canvas');
    testCanvas.id = (`cvs-${canvasId}-${Math.random().toString(36).substring(7)}`);
    testCanvas.className = `test-canvas ${canvasId}`;
    testCanvas.width = testCanvasWidth;
    testCanvas.height = testCanvasHeight;
    // document.body.appendChild(testCanvas);
    var testCanvasCtx = testCanvas.getContext("2d");
    testCanvas.style.font = `${canvasDrawingTextFontSize}px ${fontFamily}`;
    testCanvasCtx.font = [`${canvasDrawingTextFontSize}px`, fontFamily].join(' ');
    testCanvasCtx.clearRect(0, 0, testCanvasWidth, testCanvasHeight);
    testCanvasCtx.fontFamily = fontFamily;
    testCanvasCtx.fillStyle = "#fff";
    testCanvasCtx.fillRect(0,0,testCanvas.width, testCanvas.height);
    testCanvasCtx.fillStyle = "#333333";
    testCanvasCtx.fillText(testtext, testCanvasPadding, testCanvasHeight);
  
  // console.log('before timeout');
  
  
    
    // await timeout(3000);
  // console.log('timeout done');
    document.body.appendChild(testCanvas);
      

      
    //* Get Core Measurements
    var xHeight = testCanvasCtx.measureText("x").height;
    var capHeight = testCanvasCtx.measureText("H").height;
    // var measuredTextMetrics = testCanvasCtx.measureText("Hxy");
    var measuredTextMetrics = testCanvasCtx.measureText(testtext);

    //* Make the measurements usable (cast to numbers to allow for nulls)

    let metrics = {};
    metrics.measured = {
        actualBoundingBoxAscent: _roundToFive(measuredTextMetrics.actualBoundingBoxAscent),
        actualBoundingBoxDescent: _roundToFive(measuredTextMetrics.actualBoundingBoxDescent),
        actualBoundingBoxLeft: _roundToFive(measuredTextMetrics.actualBoundingBoxLeft),
        actualBoundingBoxRight: _roundToFive(measuredTextMetrics.actualBoundingBoxRight),
        fontBoundingBoxAscent: _roundToFive(measuredTextMetrics.fontBoundingBoxAscent),
        fontBoundingBoxDescent: _roundToFive(measuredTextMetrics.fontBoundingBoxDescent),
        width: _roundToFive(measuredTextMetrics.width)
    };
  
    
    const fontSizeMultiplicand = fontSizePx / canvasDrawingTextFontSize;
  
    const {
        actualBoundingBoxAscent,
        // actualBoundingBoxDescent,
        // actualBoundingBoxLeft,
        // actualBoundingBoxRight,
        fontBoundingBoxAscent,
        fontBoundingBoxDescent,
    } = metrics.measured;
    metrics.calculated = {
        gapAboveText: _roundToFive((fontBoundingBoxAscent - actualBoundingBoxAscent) * fontSizeMultiplicand),
        gapBelowText: _roundToFive(fontBoundingBoxDescent * fontSizeMultiplicand),
        textHeight: _roundToFive(actualBoundingBoxAscent * fontSizeMultiplicand),
        totalHeight: _roundToFive((fontBoundingBoxAscent + fontBoundingBoxDescent) * fontSizeMultiplicand),
    };
    const {
        gapBelowText, gapAboveText, textHeight, totalHeight
    } = metrics.calculated;

    metrics.calculated.gapBelowTextPercent = _roundToFive(gapBelowText / totalHeight);
    metrics.calculated.gapAboveTextPercent = _roundToFive(gapAboveText / totalHeight);
    metrics.calculated.gapTopBottomRatio = _roundToFive(gapAboveText / gapBelowText);
    metrics.calculated.textHeightPercent = _roundToFive(textHeight / totalHeight);
    metrics.calculated.baselineMarginTop = gapBelowText - gapAboveText;

    if(removeCalculationCanvases === true){
        testCanvas.remove(); // cleanup
    }

    return metrics;
  
  
    
};

const setFontState = async(fontFamily, fontSize, fontLabel, type = TYPE_CURRENT_FONT) => {
    if(fontFamily){
        console.log('about to get text metrics')
        const metrics = await getTextMetrics(fontFamily, fontSize);
      console.log('metrics received');
        state[type] = {
            label: fontLabel ? fontLabel : fontFamily,
            fontFamily,
            fontSize,
            metrics
        }
    }
    else {
        state[type] = {
            ...INITIAL_STATE[type]
        }
    }
  return true;
}


const watchForFontChange = async() => {
    document.addEventListener('input', async(event) => {
        if (event.target.id !== 'font-picker') return; // Only run on the font change menu

        let label = null;
        if(
            event.target.options.length &&
            typeof event.target.options[event.target.selectedIndex] !== 'undefined' &&
            event.target.options[event.target.selectedIndex].text
        ) {
            label = event.target.options[event.target.selectedIndex].text;
        }

        // For now just grab font size from the default font state, but probably will change later
        const fontFamily = event.target.value;
        const fontSize = state[TYPE_DEFAULT_FONT].fontSize;
        await setFontState(fontFamily, fontSize, label);
        console.log('font changed', state);

        //* Set the font families in the display
        if(fontFamily){
            document.getElementById(`unadjusted-text`).style.fontFamily = fontFamily;
            document.getElementById(`adjusted-text`).style.fontFamily = fontFamily;
        }
        else {
            document.getElementById(`unadjusted-text`).style.fontFamily = null;
            document.getElementById(`adjusted-text`).style.fontFamily = null;
        }


        //* Calculate the adjustments for the new font compared to the baseline
        // const currentFontSize = parseInt(state.currentFont.fontSize,10);
        const defaultFontMetrics = state.defaultFont.metrics;
        const currentFontMetrics = state.currentFont.metrics;
        // const fontSizeAdjustPx = defaultFontMetrics.calculated.textHeight - currentFontMetrics.calculated.textHeight;
        // const fontSizeAdjustPcnt = _roundToFive(fontSizeAdjustPx / currentFontMetrics.calculated.textHeight);


        //* Apply the adjustments
        // const newFontSize = currentFontSize + (currentFontSize * fontSizeAdjustPcnt);
        // console.log('newFontSize', newFontSize);
        const textToAdjust = document.getElementById(`adjusted-text`);
        // const fontSizeStr = `${newFontSize}px`;


        textToAdjust.style.marginTop = `${currentFontMetrics.calculated.baselineMarginTop}px`;

        const scaler = document.getElementById('adjusted-text-scaler');
        const scale = _roundToTwo(defaultFontMetrics.calculated.textHeight / currentFontMetrics.calculated.textHeight);
        scaler.style.transform = `scale(${scale})`;

    }, false);
}


const addFontOptionsToDropdown = () => {
    const parentSelect = document.getElementById(`font-picker`);
    for(let i=0; i < allAvailableFonts.length; i++){
        const thisOption = allAvailableFonts[i];
        if(thisOption.value){
            const label = thisOption.label ? thisOption.label : thisOption.value;
            const thisOptionTag = document.createElement("option");
            thisOptionTag.value = thisOption.value;
            const thisOptionText = document.createTextNode(label);
            thisOptionTag.appendChild(thisOptionText);
            parentSelect.appendChild(thisOptionTag);
        }
    }
}

const parseDefaultFont = async() => {
    const thisText = document.getElementById(`original-text`);

    // We might need to do some special stuff for uppercase vs non-uppercase text
    const thisTextStyle = window.getComputedStyle(thisText);
    const textTransform = thisTextStyle.getPropertyValue('text-transform');
    const marginTop = thisTextStyle.getPropertyValue('margin-top');
    console.log('marginTop', marginTop);
    const uppercase = textTransform === 'uppercase';

    const fontFamily = thisTextStyle.getPropertyValue('font-family');
    const fontSize = thisTextStyle.getPropertyValue('font-size');
    console.log('fontSize', fontSize);
    await setFontState(fontFamily, fontSize, null, TYPE_DEFAULT_FONT);

    document.getElementById(`original-text`).style.marginTop = `${state.defaultFont.metrics.calculated.baselineMarginTop}px`;

    return !! fontFamily;
}

const init = async() => {
  console.log(' ');
  console.log(' ');
  console.log(' ');
  console.log('initialized.');
    const defaultFont = await parseDefaultFont();
    if(defaultFont){
        addFontOptionsToDropdown(); // Parse JSON object into the select html tag
        await watchForFontChange();
    }
    else {
        // Handle Error -- for some reason there wasn't a font family for the default text.
    }
    document.getElementById('font-picker').disabled = false;
    
    console.log('state after init done', state);
}


//* Wait for all the base 64'd fonts to load before we run it
document.addEventListener("DOMContentLoaded", (ready => {
  init();
  // setTimeout(function(){ init(); }, 1000);
}));