Landing: wordmark thickness update (#29)
This commit is contained in:
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})`;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user