Landing: wordmark thickness update (#29)

This commit is contained in:
BFlat authored and GitHub committed 2026-09-20 20:31:18 +07:00
1 parent fc9d727ffc
commit f77f09018a
3 files changed
+134 -100

No files matched your search

@@ -4,16 +4,15 @@ import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import { heroWordmark } from "@/components/ruler/ruler-shared";
import { introClock, introTuner, onIntroReplay } from "@/lib/intro";
import { defineTunables, useTunerStore } from "@/lib/tuner";
import { useTunerStore } from "@/lib/tuner";
import {
WORDMARK_FONT,
WORDMARK_STRETCH,
wordmarkDefaults,
wordmarkTuner,
} from "./wordmark-tunables";
const WORDMARK_TEXT = "ARTCRAFT";
// Variable Archivo at its poster extreme — the Archivo Black look, but on
// the same variable family as every heading, so HeadingFlow can
// interpolate weight/width down to the display setting (620 / 118%)
// during the hero flip.
const WORDMARK_FONT = "var(--font-archivo), system-ui, sans-serif";
const WORDMARK_WEIGHT = 900;
const WORDMARK_STRETCH = "125%";
// The leading A is the brand mark, not the glyph — an inline SVG in
// currentColor, so the ruler's solid-ink dimming, hover, and press states
@@ -22,81 +21,6 @@ const WORDMARK_STRETCH = "125%";
// it rides the entire heading lifecycle.
const LOGO_ASPECT = 116.34 / 97.5;
// The logo's optical fit against the glyphs is dialed by eye: the browser
// aligns an inline SVG's BOX bottom to the text baseline, but glyph ink is
// placed from font metrics (baseline overshoot, cap forms sitting a hair
// off the geometric lines in heavy masters) — there is no CSS auto-sync
// between SVG geometry and type ink, so cap height, baseline lift, and
// side bearing are live knobs to tune and bake.
const wordmarkTuner = defineTunables("wordmark", "Wordmark", {
logoCap: {
label: "Logo cap em",
min: 0.5,
max: 1,
step: 0.005,
default: 0.7,
info: "Height of the logo-A relative to the font size — match it to the caps' visual height.",
},
logoLift: {
label: "Logo lift em",
min: -0.1,
max: 0.1,
step: 0.002,
default: -0.006,
info: "Vertical baseline correction of the logo-A: positive raises it. Syncs the SVG's box-bottom alignment with the glyphs' ink baseline.",
},
logoPad: {
label: "Logo pad em",
min: 0,
max: 0.15,
step: 0.005,
default: 0.05,
info: "Side bearing between the logo-A and the R, standing in for the glyph spacing the SVG doesn't have.",
},
bladeTuck: {
label: "Blade tuck em",
min: 0,
max: 0.5,
step: 0.01,
default: 0.12,
info: "Extra distance the sliding word starts tucked behind the blade edge — kills any sliver peeking past the clip at rest.",
},
scrimPadX: {
label: "Scrim pad x",
min: 0,
max: 320,
step: 8,
default: 128,
info: "How far the contrast scrim behind the center stack extends horizontally beyond the content.",
},
scrimPadY: {
label: "Scrim pad y",
min: 0,
max: 240,
step: 8,
default: 88,
info: "How far the contrast scrim extends vertically beyond the content.",
},
scrimBg: {
label: "Scrim peak %",
min: 40,
max: 95,
step: 1,
default: 78,
info: "Peak page-background strength at the scrim's center — the contrast pocket the wordmark and copy sit in over the galaxy.",
},
});
function wordmarkDefaults(): { [K in keyof typeof wordmarkTuner.defs]: number } {
const out = {} as { [K in keyof typeof wordmarkTuner.defs]: number };
for (const key of Object.keys(wordmarkTuner.defs) as Array<
keyof typeof wordmarkTuner.defs
>) {
out[key] = wordmarkTuner.defs[key].default;
}
return out;
}
// The contrast pocket behind the center stack — a radial page-bg gradient
// over the galaxy, live-tunable (pads + peak strength) in the Wordmark
// tuner group.
@@ -226,8 +150,8 @@ export default function HeroMasthead() {
probe.style.cssText =
"position:absolute;left:-9999px;top:0;visibility:hidden;white-space:pre;font-size:100px;line-height:1;";
probe.style.fontFamily = WORDMARK_FONT;
probe.style.fontWeight = String(WORDMARK_WEIGHT);
probe.style.fontStretch = WORDMARK_STRETCH;
probe.style.fontWeight = String(wm.weight);
probe.style.fontStretch = `${WORDMARK_STRETCH}%`;
// Letter 0 is the logo (fixed em advance), so only RTCRAFT is
// probed; the logo's advance joins the per-px run analytically.
probe.textContent = WORDMARK_TEXT.slice(1);
@@ -436,8 +360,8 @@ export default function HeroMasthead() {
className="relative whitespace-pre text-ink-strong"
style={{
fontFamily: WORDMARK_FONT,
fontWeight: WORDMARK_WEIGHT,
fontStretch: WORDMARK_STRETCH,
fontWeight: wm.weight,
fontStretch: `${WORDMARK_STRETCH}%`,
// Pre-measure fallback: analytically ≈ box width / the word's
// advance run (~5.6em), so even the first server-rendered frame
// sits at the right scale — a bare vw fallback rendered the
@@ -0,0 +1,97 @@
import { defineTunables } from "@/lib/tuner";
// The hero wordmark's type: variable Archivo, so HeadingFlow can morph the
// letters down to the headings' display setting during the hero flip
// (one family, weight and width genuinely interpolate).
export const WORDMARK_FONT = "var(--font-archivo), system-ui, sans-serif";
// Poster width — the Archivo Black stretch — held constant; the resting
// weight is a live knob (see below).
export const WORDMARK_STRETCH = 125;
// The headings' display cut the flip lands on (mirrors .font-display).
export const DISPLAY_WEIGHT = 620;
export const DISPLAY_STRETCH = 118;
// The logo's optical fit against the glyphs is dialed by eye: the browser
// aligns an inline SVG's BOX bottom to the text baseline, but glyph ink is
// placed from font metrics (baseline overshoot, cap forms sitting a hair
// off the geometric lines in heavy masters) — there is no CSS auto-sync
// between SVG geometry and type ink, so cap height, baseline lift, side
// bearing and stroke weight are live knobs to tune and bake.
export const wordmarkTuner = defineTunables("wordmark", "Wordmark", {
weight: {
label: "Type weight",
min: 400,
max: 900,
step: 10,
default: 750,
info: "Resting font weight of the letters. The logo-A is a fixed-stroke SVG (about 0.22 of its cap height), so the letters' stems are dialed down from the 900 poster extreme to match it.",
},
logoCap: {
label: "Logo cap em",
min: 0.5,
max: 1,
step: 0.005,
default: 0.7,
info: "Height of the logo-A relative to the font size — match it to the caps' visual height.",
},
logoLift: {
label: "Logo lift em",
min: -0.1,
max: 0.1,
step: 0.002,
default: -0.006,
info: "Vertical baseline correction of the logo-A: positive raises it. Syncs the SVG's box-bottom alignment with the glyphs' ink baseline.",
},
logoPad: {
label: "Logo pad em",
min: 0,
max: 0.15,
step: 0.005,
default: 0.05,
info: "Side bearing between the logo-A and the R, standing in for the glyph spacing the SVG doesn't have.",
},
bladeTuck: {
label: "Blade tuck em",
min: 0,
max: 0.5,
step: 0.01,
default: 0.12,
info: "Extra distance the sliding word starts tucked behind the blade edge — kills any sliver peeking past the clip at rest.",
},
scrimPadX: {
label: "Scrim pad x",
min: 0,
max: 320,
step: 8,
default: 128,
info: "How far the contrast scrim behind the center stack extends horizontally beyond the content.",
},
scrimPadY: {
label: "Scrim pad y",
min: 0,
max: 240,
step: 8,
default: 88,
info: "How far the contrast scrim extends vertically beyond the content.",
},
scrimBg: {
label: "Scrim peak %",
min: 40,
max: 95,
step: 1,
default: 78,
info: "Peak page-background strength at the scrim's center — the contrast pocket the wordmark and copy sit in over the galaxy.",
},
});
export function wordmarkDefaults(): {
[K in keyof typeof wordmarkTuner.defs]: number;
} {
const out = {} as { [K in keyof typeof wordmarkTuner.defs]: number };
for (const key of Object.keys(wordmarkTuner.defs) as Array<
keyof typeof wordmarkTuner.defs
>) {
out[key] = wordmarkTuner.defs[key].default;
}
return out;
}
@@ -22,6 +22,12 @@ import {
rulerLookTuner,
rulerMotionTuner,
} from "./ruler-tunables";
import {
DISPLAY_STRETCH,
DISPLAY_WEIGHT,
WORDMARK_STRETCH,
wordmarkTuner,
} from "@/components/landing/wordmark-tunables";
// The ruler's heading lifecycle. Each section's heading is a word with
// three homes, all in the ruler gutter:
@@ -48,10 +54,11 @@ import {
// the top current-heading slot as the wordmark is about to duck under the
// nav (title-condenses-into-header), no queue home and no rail ride. From
// the stack onward it behaves like any section (demotes when FEATURES
// flips in). The hero letters render variable Archivo at the poster
// extreme (wght 900 / wdth 125% — the Archivo Black look) and morph to
// the headings' display setting (620 / 118%) during the flip: one
// variable family, so weight and width genuinely interpolate.
// flips in). The hero letters render variable Archivo at the wordmark's
// resting cut (tunable weight / wdth 125%, dialed to the logo mark's
// stroke) and morph to the headings' display setting (620 / 118%) during
// the flip: one variable family, so weight and width genuinely
// interpolate.
// A letter's pose on screen. x/y are the letter center in viewport px.
type Pose = {
@@ -473,16 +480,22 @@ export default function HeadingFlow({
}px, ${
y - (heroWordmark.baseDocY[i] - scrollY)
}px, 0) rotate(${rot}deg) scale(${scale * scaleFix})`;
// Variable-font morph: the wordmark's poster cut (wght 900,
// wdth 125%) eases into the headings' display setting
// (620, 118%) letter by letter with the flight — the weight
// difference lands unnoticed inside the motion. Gated on real
// flip progress: the REST branch also runs this pass with
// e = 1 (from == to), and ungated it rendered the resting
// wordmark at 620, snapping to 900 the instant a flip began.
// Variable-font morph: the wordmark's resting cut eases into
// the headings' display setting letter by letter with the
// flight — the weight difference lands unnoticed inside the
// motion. Gated on real flip progress: the REST branch also
// runs this pass with e = 1 (from == to), and ungated it
// rendered the resting wordmark at the display weight,
// snapping back the instant a flip began.
const wp = flipP > 0 ? e : 0;
el.style.fontWeight = String(Math.round(900 - 280 * wp));
el.style.fontStretch = `${(125 - 7 * wp).toFixed(1)}%`;
const w0 = wordmarkTuner.read().weight;
el.style.fontWeight = String(
Math.round(w0 - (w0 - DISPLAY_WEIGHT) * wp),
);
el.style.fontStretch = `${(
WORDMARK_STRETCH -
(WORDMARK_STRETCH - DISPLAY_STRETCH) * wp
).toFixed(1)}%`;
} else {
el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) rotate(${rot}deg) scale(${scale})`;
}