Lowtech font metrics

by phloe

HTML

<link rel="stylesheet" href="https://www.dr.dk/global/publik.css" />


<select id="fonts">
  <option>DR Publik</option>
</select>

<table>
  <caption>CSS vars</caption>
  <tbody>
    <tr>
      <th>--cap-height<span aria-hidden="true">:</span></th>
      <td><span id="vars__cap_height">0.718</span><span aria-hidden="true">;</span></td>
    </tr>
    <tr>
      <th>--x-height<span aria-hidden="true">:</span></th>
      <td><span id="vars__x_height">0.538</span><span aria-hidden="true">;</span></td>
    </tr>
  </tbody>
</table>

<div id="sample">
  <div id="sample__text">
    Hx
  </div>
</div>

<div id="test">
  <button>
    Click here
  </button>
  <br/>
  <button class="button--cap">
    Click here
  </button>
</div>

<div class="measure">
  <div id="linebox">
    <span>M</span>
    <div id="metrics" />
  </div>
  <div id="lineheight">
    <span>M</span>
  </div>
</div>

SCSS

html {
  --font-family: "DR Publik";
	--xxx-small: 0.125;
	--xx-small: 0.25;
	--x-small: 0.375;
	--small: 0.625;
	--medium: 1;
	--large: 1.625;
	--x-large: 2.625;
	--xx-large: 4.25;
	--xxx-large: 6.875;
	--xxxx-large: 11.0625;
  
  --fg-color: #333333;
	--bg-color: #EEEEEE;
  --secondary-fg-color: #BBBBBB;
	--tertiary-fg-color: #999999;

  --horizontal-line: linear-gradient(90deg, var(--secondary-fg-color) 50%, transparent 50%) 0 0 / 0.5rem 0.0625rem repeat-x;
  --vertical-line: linear-gradient(var(--tertiary-fg-color), var(--tertiary-fg-color)) 0 0 / 0.0625rem 1rem no-repeat;
  --sample-linebox-bg-color: #D0D0D0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100%;
  background-color: var(--bg-color);
}

body {
  max-width: 40rem;
  height: min-content;
  background: transparent;
}

.measure {
  font-family: var(--font-family);
  pointer-events: none;
  /*
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
  */
}

#lineheight {
  position: absolute;
  font-size: 10000px;
  line-height: normal;
  background-color: yellow;
}
#linebox {
  position: absolute;
  font-size: 10000px;
  line-height: 1ex;
  display: flex;
  align-items: baseline;
  background-color: yellow;
  
   &::after {
    content: "";
    visibility: hidden;
    font-size: 0;
    height: 100%;
    width: 0;
    display: grid;
    align-items: end;
  }
}

#metrics {
  position: absolute;
  overflow: hidden;
  width: 1cap;
  height: 1ex;
}

#sample {
  font-family: var(--font-family);
  --size: 6;
  --padding-top: calc(3 * 1rem);
  font-size: calc(var(--size) / var(--char-height) * 1rem);
  height: calc(var(--size) * 1rem);
  padding-block: var(--padding-top) 0;

  background:
    var(--horizontal-line),
    var(--horizontal-line),
    var(--horizontal-line),
    var(--horizontal-line),
    var(--vertical-line),
    var(--vertical-line),
    var(--vertical-line);
 ...

JavaScript

function round(num, precision = 5) {
  return num.toFixed(precision).replace(/\.?0+$/, "");
}

function setVar(name, value) {
  document.documentElement.style.setProperty(name, value);
}

function setOutputVar(element, value) {
  element.innerHTML = round(value);
}

function getHeight(element) {
  return element.getBoundingClientRect().height;
}

function updateMetrics(cap, ex, offset) {
  setVar("--char-height", cap);
  setVar("--cap-height", cap);
  setVar("--x-height", ex);
  setVar("--offset", offset);

  setOutputVar(vars__cap_height, cap);
  setOutputVar(vars__x_height, ex);
}

function getMetrics() {
  const em = parseFloat(window.getComputedStyle(metrics).getPropertyValue("font-size"), 10);
  let {
    width: cap,
    height: ex
  } = metrics.getBoundingClientRect();

  let offset = (getHeight(linebox) - ex) - ((cap - ex) / 2);

  cap /= em;
  ex /= em;
  offset /= em;

  console.log(getHeight(linebox), em, cap, ex);

  updateMetrics(cap, ex, offset);
  scheduleCheck = false;
}

let scheduleCheck = false;

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    if (!scheduleCheck) {
      scheduleCheck = true;
      requestAnimationFrame(getMetrics);
    }
  }
});

resizeObserver.observe(metrics);

getMetrics();

function blobToDataURL(blob) {
  return new Promise((resolve) => {
    var reader = new FileReader();
    reader.addEventListener("load", async function(event) {
      resolve(event.target.result);
    });
    reader.readAsDataURL(blob);
  });
}

const formatMap = {
  ttf: "truetype",
  otf: "opentype",
  woff: "woff",
  woff2: "woff2"
};

function createFontStyles(name, file) {
  const {
    url,
    format
  } = file || {};
  const css = `
			${
  url && format ?
        `@font-face {
					font-family: "${name}";
					font-display: swap;
					src: url("${url}") format("${formatMap[format]}");
				}` : ""
  }
			`;

  const style = document.createElement("style");

  style.innerHTML = css;

 ...