import fs from 'fs';
import { marked } from 'marked'
import { markedSmartypants } from "marked-smartypants";
import path from 'path';
marked.setOptions({
headerIds: false,
mangle: false,
gfm: true
});
marked.use(markedSmartypants());
const STYLE = fs.readFileSync('./style.css', 'utf8');
const DOCUMENT_TEMPLATE = fs.readFileSync('./_templates/document.template.html', 'utf8');
const INTRO_TEMPLATE = fs.readFileSync('./_templates/intro.template.html', 'utf8');
const SPECIMEN_TEMPLATE = fs.readFileSync('./_templates/specimen.template.html', 'utf8');
const WEIGHTS_TEMPLATE = fs.readFileSync('./_templates/weights.template.html', 'utf8');
const VIEWER_TEMPLATE = fs.readFileSync('./_templates/viewer.template.html', 'utf8');
const METRICS_TEMPLATE = fs.readFileSync('./_templates/metrics.template.html', 'utf8');
const SAMPLES_TEMPLATE = fs.readFileSync('./_templates/samples.template.html', 'utf8');
const CHARACTERS_TEMPLATE = fs.readFileSync('./_templates/characters.template.html', 'utf8');
const LETTERS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
const CHARACTERS = ['!', '@', '#', '&', '*', '?', '’', '“', '”']
const TITLE = 'Olivia Sans'
const SUB_TITLE = 'Olivia Sans is a simple geometric sans serif typeface. But that’s probably obvious.'
const DESCRIPTION = 'The world doesn’t need another typeface, but I wanted to make one anyways. Olivia Sans was made for my own design projects my daughters’ ones. The typeface consists of 126 glyphs and a weight range of regular to bold. It’s not much, but that’s all that was needed. The design aims for strong continuity across each glyph with consistent angles and thicks/thins. Given this was my first ever typeface I made, I decided to <a href="https://pjonori.blog/posts/making-a-typeface/">journal my process</a>.'
const METRICS = {ascender: 800, capHeight: 700, xHeight: 515, baseline: 0, descender: -200}
const METRICS_SAMPLE = 'Rig'
const SAMPLES = [
'The way you do anything is the way you do everything.',
'_Purveyor of fine typos._',
'‹ Back',
'79°',
'A system is a system because its connections work together. As a system grows, it becomes increasingly difficult to understand how everything works—or if it works at all. This rears its head in general unintelligibility, brittleness and unpredictability. At which point the system has officially devolved into a pile of stuff.\n\nUnpredictability in particular is scary. Because unpredictability, is well, unpredictable. Unpredictable can result in good, bad, or horrible. It can be a combination of the three one minute, then none of the above the next. Which is chaos. And chaos is bad.',
'3.14159',
'The fairy game is fun, but it’s almost done. \nWe go to the mall, but don’t get a doll. \nWe got taught, it was quite a lot. \nThe four of us walked through the door. \nOn that first day and we all said, “hooray”!\n\n—_Fairy Game_, a poem by Olivia'
]
const WEIGHT_RANGE = {min: 400, max: 700}
let letters = ''
for(let i = 0; i < LETTERS.length; i++) {
letters += '<div class="glyph">' + LETTERS[i].toUpperCase() + '</div>'
letters += '<div class="glyph">' + LETTERS[i].toLowerCase() + '</div>'
}
let letterPairings = ''
for(let i = 0; i < LETTERS.length; i++) {
letterPairings += '<div class="glyph">' + LETTERS[i].toUpperCase() + '' + LETTERS[i].toLowerCase() + '</div>'
}
let numbers = ''
for(let i = 0; i < NUMBERS.length; i++) {
numbers += '<div class="glyph">' + NUMBERS[i] + '</div>'
}
let characters = ''
for(let i = 0; i < CHARACTERS.length; i++) {
characters += '<div class="glyph">' + CHARACTERS[i] + '</div>'
}
let glyphs = letters.concat(numbers).concat(characters);
let intro = INTRO_TEMPLATE.replace(new RegExp('%title%', 'g'), TITLE);
intro = intro.replace(new RegExp('%sub-title%', 'g'), SUB_TITLE);
intro = intro.replace(new RegExp('%description%', 'g'), DESCRIPTION);
let specimen = SPECIMEN_TEMPLATE.replace(new RegExp('%specimen-letters%', 'g'), letterPairings);
specimen = specimen.replace(new RegExp('%specimen-numbers%', 'g'), numbers);
specimen = specimen.replace(new RegExp('%specimen-characters%', 'g'), characters);
let weights = WEIGHTS_TEMPLATE.replace(new RegExp('%weights-min%', 'g'), WEIGHT_RANGE.min);
weights = weights.replace(new RegExp('%weights-max%', 'g'), WEIGHT_RANGE.max);
weights = weights.replace(new RegExp('%weights-glyphs%', 'g'), letters);
let viewer = VIEWER_TEMPLATE.replace(new RegExp('%viewer-glyphs%', 'g'), glyphs);
let metrics = METRICS_TEMPLATE.replace(new RegExp('%cap-height%', 'g'), METRICS.capHeight);
metrics = metrics.replace(new RegExp('%x-height%', 'g'), METRICS.xHeight);
metrics = metrics.replace(new RegExp('%baseline%', 'g'), METRICS.baseline);
metrics = metrics.replace(new RegExp('%descender%', 'g'), METRICS.descender);
metrics = metrics.replace(new RegExp('%metrics-sample%', 'g'), METRICS_SAMPLE);
let sampleMarkup = '';
for(let i = 0; i < SAMPLES.length; i++) {
sampleMarkup+='<div class="sample ' + 'sample-' + Number(i+1) + '">' + marked(SAMPLES[i]) + '</div>'
}
let samples = SAMPLES_TEMPLATE.replace(new RegExp('%samples%', 'g'), sampleMarkup);
let document = DOCUMENT_TEMPLATE.replace(new RegExp('%title%', 'g'), TITLE);
document = document.replace(new RegExp('%style%', 'g'), '<style>' + STYLE + '</style>');
document = document.replace(new RegExp('%intro%', 'g'), intro);
document = document.replace(new RegExp('%specimen%', 'g'), specimen);
document = document.replace(new RegExp('%weights%', 'g'), weights);
document = document.replace(new RegExp('%viewer%', 'g'), viewer);
document = document.replace(new RegExp('%metrics%', 'g'), metrics);
document = document.replace(new RegExp('%samples%', 'g'), samples);
document = document.replace(new RegExp('%characters%', 'g'), CHARACTERS_TEMPLATE);
fs.writeFileSync('index.html', document);
<section class="hero">
<h1>%title%</h1>
<h2>%sub-title%</h2>
<p class="columns">%description%</p>
</section>
<section class="specimen-container">
<div class="specimen specimen-regular">
<div class="specimen-group">
%specimen-letters%
</div>
<div class="specimen-group">
%specimen-numbers%
</div>
<div class="specimen-group">
%specimen-characters%
</div>
</div>
<div class="specimen specimen-bold">
<div class="specimen-group">
%specimen-letters%
</div>
<div class="specimen-group">
%specimen-numbers%
</div>
<div class="specimen-group">
%specimen-characters%
</div>
</div>
</section>
<section class="variable">
<div class="caption">Font weights: %weights-min%—%weights-max%</div>
<div class="letters">
%weights-glyphs%
</div>
</section>
<section class="letters-container" style="padding: 5rem 0;">
<div class="scroll">Scroll →</div>
<div class="letter-display">
%viewer-glyphs%
</div>
</section>
<section>
<div class="metrics">
<div class='metric cap-height'><span>%cap-height%</span></div>
<div class='metric x-height'><span>%x-height%</span></div>
<div class='metric baseline'><span>%baseline%</span></div>
<div class='metric descender'><span>%descender%</span></div>
<div class="text">%metrics-sample%</div>
</div>
</section>
<section class="samples-container">
%samples%
</section>
<section class="special-characters">
<div class="character-chevron">›</div><div class="character-arrow">↑</div><div class="character-infinity">∞</div><div class="character-ampersand">&</div>
</section>
:root {
--font-size-3000: clamp(150px, 42vw, 640px);
--font-size-2000: clamp(64px, 31vw, 480px);
--font-size-1000: clamp(36px, 15vw, 245px);
--font-size-900: clamp(32px, 8vw, 112px);
--font-size-750: clamp(24px, 4vw, 64px);
--font-size-500: clamp(24px, 4vw, 48px);
--font-size-300: clamp(20px, 3vw, 32px);
--font-size-250: clamp(14px, 1.2vw, 18px);
--font-size-100: clamp(13px, 1.1vw, 16px);
--padding: clamp(16px, 5vw, 80px);
--padding-loose: clamp(32px, 8vw, 160px);
--color-positive-300: #fafafa;
--color-positive-200: #ccc;
--color-positive-100: #222;
--color-positive-0: #000;
--color-negative: #fff;
--color-dark: #000;
--color-blue: #2070BB;
--color-red: #EE450F;
--color-yellow: #FED915;
--color-purple: #CC72F6;
}
@media (prefers-color-scheme: dark) {
:root {
--color-positive-300: #333;
--color-positive-200: #ccc;
--color-positive-100: #efefef;
--color-positive-0: #fff;
--color-negative: #222;
--color-dark: #000;
--color-blue: #81c2ff;
--color-red: #EE450F;
--color-yellow: #FED915;
--color-purple: #8304be;
}
}
@font-face {
font-family: "Olivia Sans";
src:
url("OliviaSans-VariableVF.ttf") format("truetype-variations");
font-weight: 400 700;
}
@keyframes FRA{
0%{
font-weight: 400;
color: var(--color-blue);
}
50%{
font-weight: 700;
color: var(--color-red);
}
100%{
font-weight: 400;
color: var(--color-blue);
}
}
html {
overflow-x: hidden;
}
body, html {
padding: 0;
margin: 0;
font-size: 16px;
-webkit-font-smoothing: antialiased;
}
body {
font-family: 'Olivia Sans';
padding: var(--padding);
margin: 0 auto;
line-height: 1.5;
font-synthesis: none;
/*font-variation-settings: "ital" 9;*/
overflow-anchor: none;
background: var(--color-negative);
color: var(--color-positive-0);
max-width: none;
padding-top: 0;
position: static;
}
header {
max-width: 1000px;
margin: 0 auto;
position: relative;
}
a {
color: inherit;
}
em {
font-variation-settings: "ital" 10;
}
section.hero {
margin-top: 10rem;
}
section {
padding: var(--padding);
box-sizing: border-box;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
}
header + section, section + section {
margin-top: var(--padding-loose);
}
h1 {
font-size: var(--font-size-1000);
letter-spacing: -0.025em;
line-height: .9;
text-indent: -.07em;
font-weight: 600;
margin: 0;
}
h2 {
max-width: 40ch;
font-weight: 400;
font-size: var(--font-size-500);
letter-spacing: -0.01em;
line-height: 1.25;
margin-top: 0.25em;
}
.caption {
font-size: var(--font-size-100);
font-weight: 700;
}
.letters {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(4ch, 1fr)); /* this works in safari and chrome */
gap: 0em;
font-size: 2em;
}
.letters .glyph {
display: flex;
/*height: 4ch;*/
width: 100%;
justify-content: center;
align-items: center;
border: 1px solid var(--color-positive-300);
background: var(--color-negative);
margin-top: -1px;
user-select: none;
aspect-ratio: 1;
}
.columns {
font-weight: 500;
columns: 40ch auto;
column-gap: 2em;
font-size: var(--font-size-250);
width: 80%;
min-width: 320px;
}
.specimen-container {
background: var(--color-positive-100);
display: flex;
flex-direction: column;
gap: 2em;
font-size: var(--font-size-750);
}
.specimen {
display: flex;
flex-direction: column;
gap: .5em;
}
.specimen-group {
display: flex;
flex-wrap: wrap;
color: var(--color-positive-300);
line-height: 1.1;
gap: .15em;
}
.specimen-bold {
font-weight: 700;
}
.variable {
display: flex;
flex-direction: column;
padding-left: 0;
padding-right: 0;
gap: 1rem;
}
.variable .letters {
font-size: var(--font-size-500);
grid-template-columns: repeat(auto-fit, minmax(4ch, 1fr));
color: var(--color-blue);
}
.variable .glyph {
animation: FRA forwards 6s infinite;
animation-timing-function: ease-in-out;
}
.variable .glyph:nth-child(2) { animation-delay: 30ms; }.variable .glyph:nth-child(3) { animation-delay: 60ms; }.variable .glyph:nth-child(4) { animation-delay: 90ms; }.variable .glyph:nth-child(5) { animation-delay: 120ms; }.variable .glyph:nth-child(6) { animation-delay: 150ms; }.variable .glyph:nth-child(7) { animation-delay: 180ms; }.variable .glyph:nth-child(8) { animation-delay: 210ms; }.variable .glyph:nth-child(9) { animation-delay: 240ms; }.variable .glyph:nth-child(10) { animation-delay: 270ms; }.variable .glyph:nth-child(11) { animation-delay: 300ms; }.variable .glyph:nth-child(12) { animation-delay: 330ms; }.variable .glyph:nth-child(13) { animation-delay: 360ms; }.variable .glyph:nth-child(14) { animation-delay: 390ms; }.variable .glyph:nth-child(15) { animation-delay: 420ms; }.variable .glyph:nth-child(16) { animation-delay: 450ms; }.variable .glyph:nth-child(17) { animation-delay: 480ms; }.variable .glyph:nth-child(18) { animation-delay: 510ms; }.variable .glyph:nth-child(19) { animation-delay: 540ms; }.variable .glyph:nth-child(20) { animation-delay: 570ms; }.variable .glyph:nth-child(21) { animation-delay: 600ms; }.variable .glyph:nth-child(22) { animation-delay: 630ms; }.variable .glyph:nth-child(23) { animation-delay: 660ms; }.variable .glyph:nth-child(24) { animation-delay: 690ms; }.variable .glyph:nth-child(25) { animation-delay: 720ms; }.variable .glyph:nth-child(26) { animation-delay: 750ms; }.variable .glyph:nth-child(27) { animation-delay: 780ms; }.variable .glyph:nth-child(28) { animation-delay: 810ms; }.variable .glyph:nth-child(29) { animation-delay: 840ms; }.variable .glyph:nth-child(30) { animation-delay: 870ms; }.variable .glyph:nth-child(31) { animation-delay: 900ms; }.variable .glyph:nth-child(32) { animation-delay: 930ms; }.variable .glyph:nth-child(33) { animation-delay: 960ms; }.variable .glyph:nth-child(34) { animation-delay: 990ms; }.variable .glyph:nth-child(35) { animation-delay: 1020ms; }.variable .glyph:nth-child(36) { animation-delay: 1050ms; }.variable .glyph:nth-child(37) { animation-delay: 1080ms; }.variable .glyph:nth-child(38) { animation-delay: 1110ms; }.variable .glyph:nth-child(39) { animation-delay: 1140ms; }.variable .glyph:nth-child(40) { animation-delay: 1170ms; }.variable .glyph:nth-child(41) { animation-delay: 1200ms; }.variable .glyph:nth-child(42) { animation-delay: 1230ms; }.variable .glyph:nth-child(43) { animation-delay: 1260ms; }.variable .glyph:nth-child(44) { animation-delay: 1290ms; }.variable .glyph:nth-child(45) { animation-delay: 1320ms; }.variable .glyph:nth-child(46) { animation-delay: 1350ms; }.variable .glyph:nth-child(47) { animation-delay: 1380ms; }.variable .glyph:nth-child(48) { animation-delay: 1410ms; }.variable .glyph:nth-child(49) { animation-delay: 1440ms; }.variable .glyph:nth-child(50) { animation-delay: 1470ms; }.variable .glyph:nth-child(51) { animation-delay: 1500ms; }.variable .glyph:nth-child(52) { animation-delay: 1530ms; }.variable .glyph:nth-child(53) { animation-delay: 1560ms; }.variable .glyph:nth-child(54) { animation-delay: 1590ms; }.variable .glyph:nth-child(55) { animation-delay: 1620ms; }.variable .glyph:nth-child(56) { animation-delay: 1650ms; }.variable .glyph:nth-child(57) { animation-delay: 1680ms; }.variable .glyph:nth-child(58) { animation-delay: 1710ms; }.variable .glyph:nth-child(59) { animation-delay: 1740ms; }.variable .glyph:nth-child(60) { animation-delay: 1770ms; }.variable .glyph:nth-child(61) { animation-delay: 1800ms; }.variable .glyph:nth-child(62) { animation-delay: 1830ms; }.variable .glyph:nth-child(63) { animation-delay: 1860ms; }.variable .glyph:nth-child(64) { animation-delay: 1890ms; }.variable .glyph:nth-child(65) { animation-delay: 1920ms; }.variable .glyph:nth-child(66) { animation-delay: 1950ms; }
.samples-container {
padding: 5em 0;
display: flex;
flex-wrap: wrap;
}
.sample {
padding: var(--padding-loose) var(--padding);
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
flex-direction: column;
}
.sample > * {
max-width: 100%;
}
.sample p {
margin: 0;
}
.sample p + p {
margin-top: 1em;
}
.sample-1, .sample-7 {
width: 100%;
min-width: 320px;
}
.sample-2, .sample-5, .sample-6 {
width: 50%;
min-width: 320px;
}
.sample-3, .sample-4 {
width: 25%;
min-width: 160px;
}
.sample-1 {
background: var(--color-yellow);
color: var(--color-dark);
}
.sample-1 p {
width: 20ch;
font-size: var(--font-size-750);
font-weight: 400;
}
.sample-2 {
background: var(--color-red);
font-size: var(--font-size-300);
color: var(--color-negative);
}
.sample-3 {
font-size: var(--font-size-250);
font-weight: 500;
}
.sample-4 {
background: var(--color-blue);
font-size: var(--font-size-900);
color: var(--color-negative);
}
.sample-5 {
background: var(--color-positive-100);
}
.sample-5 p {
width: 40ch;
font-size: var(--font-size-250);
color: var(--color-positive-300);
font-weight: 500;
line-height: 1.5;
max-width: 100%;
}
.sample-6 {
background: var(--color-positive-300);
font-size: var(--font-size-900);
font-weight: 700;
}
.sample-7 {
font-size: var(--font-size-300);
font-weight: 600;
background: var(--color-purple);
}
.sample-7 p + p {
display: block;
font-size: var(--font-size-100);
font-weight: 600;
}
.metrics {
font-size: var(--font-size-2000);
line-height: 2.2ex;
position: relative;
text-align: center;
}
.metric {
position: absolute;
width: 100%;
border-bottom: 1px solid var(--color-positive-200);
z-index: 0;
font-weight: 700;
text-align: right;
}
.metric span {
font-size: 10px;
line-height: 1.5;
display: block;
bottom: 0;
right: 0;
position: absolute;
}
.text {
position: relative;
z-index: 2;
}
.cap-height {
bottom: 1.74ex;
height: .195ex;
}
.x-height {
bottom: 1.38ex;
height: .359ex;
}
.baseline {
bottom: .384ex;
height: 1ex;
}
.descender {
height: .388ex;
bottom: 0;
}
.letters-container {
position: relative;
display: flex;
justify-content: end;
}
.letters-container .letters {
width: 50%;
}
.letter-display {
width: 100%;
position: relative;
display: flex;
overflow-x: auto;
overflow-y: visible;
gap: 32px;
flex-grow: 1;
background: var(--color-positive-300);
min-height: 500px;
}
.scroll {
position: absolute;
font-size: var(--font-size-100);
width: 100%;
z-index: 4;
font-weight: 700;
top: 6rem;
left: 1rem;
}
.letter-display .glyph {
min-width: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: var(--font-size-3000);
line-height: 1.25;
}
.letters-container .glyph {
color: var(--color-positive-0);
text-decoration: none;
}
.special-characters {
font-size: var(--font-size-2000);
font-weight: 550;
display: flex;
flex-wrap: wrap;
}
.special-characters div {
flex-basis: 50%;
aspect-ratio: 4;
display: flex;
justify-content: center;
align-items: center;
line-height: 1;
}
.character-chevron {
color: var(--color-yellow);
}
.character-arrow {
color: var(--color-positive-100);
}
.character-infinity {
color: var(--color-blue);
}
.character-ampersand {
color: var(--color-red);
}
.end {
font-size: var(--font-size-250);
display: flex;
justify-content: center;
align-items: center;
}
.end a {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: .15em;
font-weight: 700;
text-decoration: none;
}