From 73dcd63026c0738c8c22738ee24324f0a071a3ee Mon Sep 17 00:00:00 2001 From: Nika Siradze Date: Sat, 3 Oct 2026 03:08:12 +0400 Subject: [PATCH] Let a style pick its display and label fonts A style can name a display and a label font from the four bundled families (grotesk, serif, condensed, mono) on top of any pack, so a look matched from a reference can carry its typeface. The pack's colours, surface and motion stay as they were. --- remotion/src/style/theme.test.ts | 21 +++++++++++++++++++- remotion/src/style/theme.ts | 33 +++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/remotion/src/style/theme.test.ts b/remotion/src/style/theme.test.ts index 508980da..cc49a76f 100644 --- a/remotion/src/style/theme.test.ts +++ b/remotion/src/style/theme.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ENTRANCES, MOTIONS, PACKS, ROLES, resolveTheme } from "./theme"; +import { ENTRANCES, FONT_IDS, MOTIONS, PACKS, ROLES, resolveTheme } from "./theme"; describe("resolveTheme", () => { it("returns null without a style", () => { @@ -71,3 +71,22 @@ describe("motion presets", () => { } }); }); + +describe("fonts picked to match a reference", () => { + it("swap the display and label faces and leave the rest of the pack", () => { + const theme = resolveTheme({ pack: "clean", fonts: { display: "condensed", label: "mono" } }); + expect(theme?.type.display.family).toContain("Barlow Condensed"); + expect(theme?.type.label.family).toContain("Courier Prime"); + expect(theme?.color).toEqual(PACKS.clean.color); + }); + + it("keep the pack's faces when none are picked", () => { + expect(resolveTheme({ pack: "editorial" })?.type).toEqual(PACKS.editorial.type); + }); + + it("offer every font on every pack", () => { + for (const font of FONT_IDS) { + expect(resolveTheme({ pack: "riso", fonts: { display: font } })?.type.display.family).toBeTruthy(); + } + }); +}); diff --git a/remotion/src/style/theme.ts b/remotion/src/style/theme.ts index 5f9d49d9..0a237e85 100644 --- a/remotion/src/style/theme.ts +++ b/remotion/src/style/theme.ts @@ -197,9 +197,14 @@ export const MOTIONS = { export type MotionId = keyof typeof MOTIONS; +export type FontId = "grotesk" | "serif" | "condensed" | "mono"; + +export const FONT_IDS: FontId[] = ["grotesk", "serif", "condensed", "mono"]; + export interface ThemeInput { pack: PackId; motion?: MotionId; + fonts?: { display?: FontId; label?: FontId }; overrides?: DeepPartial; textures?: Theme["textures"]; } @@ -439,9 +444,35 @@ const deepMerge = (base: T, patch: unknown): T => { return out as T; }; +const FONTS: Record = { + grotesk: { + display: face("DM Sans", 700, { tracking: -2.5, lineHeight: 1.02 }), + label: face("DM Sans", 700, { lineHeight: 1.1 }), + }, + serif: { + display: face("Playfair Display", 900, { italic: true, tracking: -1, lineHeight: 1.02 }), + label: face("Playfair Display", 700, { italic: true, lineHeight: 1.1 }), + }, + condensed: { + display: face("Barlow Condensed", 700, { tracking: -0.5, lineHeight: 0.95 }), + label: face("Barlow Condensed", 600, { tracking: 0.5, lineHeight: 1.05 }), + }, + mono: { + display: face("Courier Prime", 400, { tracking: -1.5, lineHeight: 1.08, size: 0.78 }), + label: face("Courier Prime", 400, { lineHeight: 1.2 }), + }, +}; + +const withFonts = (theme: Theme, fonts: ThemeInput["fonts"]): Theme => { + const display = fonts?.display && FONTS[fonts.display]?.display; + const label = fonts?.label && FONTS[fonts.label]?.label; + if (!display && !label) return theme; + return { ...theme, type: { ...theme.type, ...(display ? { display } : {}), ...(label ? { label } : {}) } }; +}; + export const resolveTheme = (input?: ThemeInput | null): Theme | null => { if (!input) return null; - const pack: Theme = PACKS[input.pack] ?? PACKS.collage; + const pack: Theme = withFonts(PACKS[input.pack] ?? PACKS.collage, input.fonts); const base = input.motion ? { ...pack, motion: MOTIONS[input.motion] ?? pack.motion } : pack; const merged = deepMerge(base, input.overrides ?? {}); return { ...merged, textures: { ...merged.textures, ...(input.textures ?? {}) } };