Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/site/app/(home)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { notFound } from 'next/navigation';
import { Mdx } from '@/components/mdx';
import { OverviewTimeline } from '@/components/overview-timeline';
import { PostCover } from '@/components/post-cover';
import {
GlyphLab,
LineBoxLab,
LineBreakLab,
RunsStrip,
} from '@/components/text-lab';
import { blogSource } from '@/lib/blog-source';

const formatDate = (value: string) =>
Expand Down Expand Up @@ -47,7 +53,16 @@ export default async function BlogPost(props: {
</header>

<article className="prose">
<Mdx body={page.data.body} components={{ OverviewTimeline }} />
<Mdx
body={page.data.body}
components={{
OverviewTimeline,
RunsStrip,
GlyphLab,
LineBreakLab,
LineBoxLab,
}}
/>
</article>
</main>
);
Expand Down
44 changes: 44 additions & 0 deletions apps/site/components/text-lab/breaks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* First-fit line breaking: the algorithm react-pdf does *not* use, kept around
* so the lab can put it next to Knuth & Plass.
*/
export const greedyLines = (
text: string,
maxWidth: number,
measure: (line: string) => number,
) => {
const tokens = text.split(/( +)/).filter(Boolean);
const lines: string[] = [];

let current = '';

for (const token of tokens) {
if (token.trim() === '') {
if (current) current += token;
continue;
}

const candidate = current + token;

if (current.trim() && measure(candidate.trimEnd()) > maxWidth) {
lines.push(current.trimEnd());
current = token;
} else {
current = candidate;
}
}

if (current.trim()) lines.push(current.trimEnd());

return lines;
};

/**
* Sum of squared leftovers, ignoring the last line. Squaring is what makes one
* badly short line cost more than several slightly short ones, which is the
* whole reason optimal breaking looks better.
*/
export const raggedness = (widths: number[], maxWidth: number) =>
widths
.slice(0, -1)
.reduce((total, width) => total + (maxWidth - width) ** 2, 0);
74 changes: 74 additions & 0 deletions apps/site/components/text-lab/font.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { useEffect, useState } from 'react';
import * as fontkit from 'fontkit';

export const ROBOTO = '/fonts/Roboto-Regular.ttf';

export type Glyph = {
id: number;
codePoints: number[];
advanceWidth: number;
path: { toSVG: () => string };
};

export type Position = {
xAdvance: number;
yAdvance: number;
xOffset: number;
yOffset: number;
};

export type GlyphRun = {
glyphs: Glyph[];
positions: Position[];
advanceWidth: number;
};

export type Font = {
unitsPerEm: number;
ascent: number;
descent: number;
lineGap: number;
layout: (
string: string,
features?: Record<string, boolean> | string[],
) => GlyphRun;
};

const pending = new Map<string, Promise<Font>>();

const load = (src: string) => {
let request = pending.get(src);

if (!request) {
request = fetch(src)
.then((response) => response.arrayBuffer())
.then((buffer) => fontkit.create(new Uint8Array(buffer)) as Font);
pending.set(src, request);
}

return request;
};

export const useFont = (src: string = ROBOTO) => {
const [font, setFont] = useState<Font | null>(null);

useEffect(() => {
let live = true;
load(src).then((loaded) => {
if (live) setFont(loaded);
});
return () => {
live = false;
};
}, [src]);

return font;
};

export const isSpace = (glyph: Glyph) => glyph.codePoints.includes(0x20);

/** Natural line height, the value textkit uses when no lineHeight is set. */
export const naturalHeight = (font: Font, fontSize: number) =>
((font.lineGap + font.ascent - font.descent) / font.unitsPerEm) * fontSize;
3 changes: 3 additions & 0 deletions apps/site/components/text-lab/fontkit.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'fontkit' {
export function create(data: Uint8Array, postscriptName?: string): unknown;
}
Loading
Loading