Dr. Pain
Dr. Pain

Reputation: 719

Make Different Fonts Display at the Same Actual Size?

When I use two different fonts in a web page at the same font-size, they often display at different actual sizes:

Text in two different fonts

This example uses two Google Fonts, Gentium and Metamorphous at the same font-size, specified as 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>

A JSBin for this example can be found here.

My understanding of specifying font-size in an absolute length like px was that the font would be scaled to match that length. My expectation is that two different fonts at the same font-size would have either matching height or matching length (I understand the aspect ratios of the fonts may be different). But it doesn't appear that either is the case here. Is there some way I can make two arbitrary fonts display at either the same height or the same length without manually calculating and applying a correction?

EDIT: An example showing the descender to ascender distance for two fonts displayed at the same font size.

enter image description here

Clearly the two distances are not the same for these two fonts as displayed.

EDIT: An example showing letters with and without accents in the two fonts:

enter image description here

Again, clearly the letters are different sizes.

EDIT: Going on what is described in this article, the issue is that font-size controls the displayed size of the em value of the font. But the em value is arbitrary (it doesn't have to correspond to anything within the font, and in particular is not necessarily the height of a lower case 'm'), and does not include the ascenders and descenders, which can be any size at all (example taken from above article):

enter image description here

so the result is that a "100px" font can be just about any effective size whatsoever. The author of the above article computed the range of effective sizes for the Google Web Fonts at the time to be 0.618 to 3.378.

Since the font metrics (such as the em size, the capitals height, the ascender and descender values) are not exposed in CSS, there doesn't seem to be any way within CSS to make two arbitrary fonts the same effective size. For any particular font, you can use a font editor to find the font metric values and use those numbers to scale the font as required. For an arbitrary font, an option is to display some text and use the measured bounding box to determine the effective size and calculate an appropriate scaling factor.

My thanks to everyone who contributed to explaining this!

Upvotes: 6

Views: 4876

Answers (4)

Zach Swinehart
Zach Swinehart

Reputation: 31

I spent a lot of time crawling through StackOverflow looking for answers for a similar situation and ended up not finding anything perfect. What I ended up doing is measuring the two fonts, and then adjusting the top margin and scale of the second font to match the first. (By using scale instead of changing the font size, it allows us to not need to re-calculate the text metrics after resizing)

I put together a couple of pens for posterity. Here's the second one, which handles the normalization of font sizes and alignments between two fonts: https://codepen.io/zacholas/pen/oNBPWga

And the first one, that only handles the measuring, is: https://codepen.io/zacholas/pen/ExZwJjx


I guess I have to paste some code in order to link to codepen, so here's all the comparison code:

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/[email protected]";
// 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);
}));

Upvotes: 2

Rob
Rob

Reputation: 15160

Font size is the size of the glyph from the ascender, such as the top of the letter 'h', to the descender, such as the bottom of the letter 'g'. If you set your font size to 20px, the length from the top of the letter 'h' to the bottom of the letter 'g' will be 20px. Some letters have terminals or spurs, the ends of a letter, may extend a px or two higher on some letters.

enter image description here

In your example, there is a px difference between the two fonts. The Metamorphous font has a mark above some letters that Gentium does not have and that is what accounts for the height difference.

You can read more here.

EDIT: See here with the "caron" above the C compared to the two Gentium letters on the right.

enter image description here

Upvotes: 1

jrrdnx
jrrdnx

Reputation: 1585

Think of the font-size not as the actual size of the individual characters themselves, but as the size of the blocks that contain each character, just like typeset letters:

typeset letters

The size of the blocks is defined in your CSS (using px, pts, ems, etc) but the actual size of the characters within those blocks can vary depending on the font used.

The actual, physical height of any given portion of the font depends on the user-defined DPI setting, current element font-size, and the particular font being used.

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

You can use the font-size-adjust property to help alter one of those fonts to scale it closer to the other: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust although its support is currently limited to Firefox: http://caniuse.com/#feat=font-size-adjust

Upvotes: 1

user3676604
user3676604

Reputation:

you should rather use something like rem then px :) as rem is a relative measure unit and px is absolute. But fonts always have a different size and imo its not possible what you want to achieve.

Upvotes: -2

Related Questions