mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 10:46:06 +00:00
Chipmunk, Giant, and Monster ship as presets on the pitchshift worklet P1 added: Chipmunk pitches up and adds sparkle, Giant pitches down with weight and a compressor to hold the extra low end together, Monster pitches down further with saturation growl and a close, tight reverb. Every param verified against the live effect registry rather than sketched — the compressor/reverb/saturate/shelf keys all match exactly. Each gets its own title treatment (font, size, tracking, hue) so the FX rack's per-preset styling coverage and hue-distance/background-uniqueness tests extend cleanly to the three new entries, and complaint-line copy in the non-voice vocabulary the audit test enforces (no speech words — "Giant" over CapCut's "Deep Voice", as the design doc records). Updates plans/audio-fx-presets.md's two limits paragraphs to record that pitch shift landed and this half of the character list now ships; Robot and Alien stay out of scope (ring modulation, still unbuilt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
235 lines
9.1 KiB
TypeScript
235 lines
9.1 KiB
TypeScript
/**
|
|
* A title treatment per preset — the label as a small piece of design rather
|
|
* than eighteen rows of the same condensed caps.
|
|
*
|
|
* The rack already letters by FAMILY (`propertyPanelFxFamily.ts`), which answers
|
|
* "what kind of thing is this". This answers a different question: a preset is a
|
|
* character, and the point of Telephone or Megaphone is that you know what it
|
|
* sounds like before you play it. Type can carry that — a bullhorn's name should
|
|
* look shouted, a tape's should look worn.
|
|
*
|
|
* Deliberately NOT a per-preset colour free-for-all. Each hue sits in the same
|
|
* narrow lightness band as the family tints so nothing reads as a status colour,
|
|
* and the panel already spends saturation on "automated" and "bypassed".
|
|
*
|
|
* A preset with no entry falls back to the neutral treatment, so the catalogue
|
|
* can grow without this file — an unstyled preset looks plain, not broken.
|
|
*/
|
|
|
|
export interface FxPresetStyle {
|
|
/** Tailwind classes for the title: weight, case, tracking, size. */
|
|
type: string;
|
|
/** The title's colour, and the bracket's left edge. */
|
|
color: string;
|
|
/**
|
|
* A real font stack, when the character wants one.
|
|
*
|
|
* Tailwind ships three generic families, and a set of presets styled only
|
|
* with those ends up variations of the same two faces. These are system
|
|
* faces with a documented fallback chain: the studio has no webfont
|
|
* pipeline, and the Google Fonts cache under `~/.cache/hyperframes` belongs
|
|
* to the CLI's composition build — reaching into it from the panel would be
|
|
* inventing a second one.
|
|
*
|
|
* Every stack ends in a generic keyword, so a machine without the named face
|
|
* still lands somewhere deliberate.
|
|
*/
|
|
family?: string;
|
|
}
|
|
|
|
/** Faces that ship with macOS and/or Windows, grouped by what they read as. */
|
|
const FACE = {
|
|
/** Narrow industrial caps — signage, stencils, equipment panels. */
|
|
condensed: '"Haettenschweiler", "Arial Narrow", Impact, sans-serif',
|
|
/** Geometric and mechanical; reads as a machine rather than a voice. */
|
|
geometric: '"Futura", "Century Gothic", "Avenir Next", sans-serif',
|
|
/** Typewriter — struck, worn, slightly irregular. */
|
|
typewriter: '"American Typewriter", "Courier New", Courier, monospace',
|
|
/** High-contrast editorial serif; broadcast and print authority. */
|
|
editorial: '"Didot", "Bodoni 72", "Playfair Display", Georgia, serif',
|
|
/** Old-style serif with real warmth — books, not headlines. */
|
|
bookish: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif',
|
|
/** Display face with no restraint at all. Theatrical, and a bit absurd. */
|
|
theatrical: '"Luminari", "Papyrus", "Comic Sans MS", fantasy',
|
|
/** Engraved caps — plaques, institutions, things bolted to walls. */
|
|
engraved: '"Copperplate", "Optima", "Perpetua Titling MT", serif',
|
|
/** Terminal type: fixed, plain, no personality of its own. */
|
|
terminal: '"SF Mono", Consolas, Menlo, ui-monospace, monospace',
|
|
} as const;
|
|
|
|
/** Plain, and what any preset without its own entry gets. */
|
|
export const FX_PRESET_STYLE_DEFAULT: FxPresetStyle = {
|
|
type: "text-[12px] uppercase tracking-wide",
|
|
color: "hsl(220, 24%, 72%)",
|
|
family: FACE.terminal,
|
|
};
|
|
|
|
export const FX_PRESET_STYLE: Record<string, FxPresetStyle> = {
|
|
// --- voice: reach for these to sound like yourself, only better -----------
|
|
// Restrained rather than plain. These are corrective, and a costume would
|
|
// promise a character they deliberately do not add — but they still get a
|
|
// face, because "no styling at all" is what every other row in the panel has.
|
|
"voice-clean": {
|
|
type: "text-[13px] font-medium tracking-tight",
|
|
color: "hsl(202, 82%, 66%)",
|
|
family: FACE.geometric,
|
|
},
|
|
"voice-broadcast": {
|
|
type: "text-[13px] font-bold uppercase tracking-[0.14em]",
|
|
color: "hsl(214, 84%, 70%)",
|
|
family: FACE.editorial,
|
|
},
|
|
"voice-warm": {
|
|
type: "text-[14px] italic tracking-normal",
|
|
color: "hsl(32, 88%, 66%)",
|
|
family: FACE.bookish,
|
|
},
|
|
|
|
// --- repair: workshop labels. Fixed, plain, nothing decorative ------------
|
|
"rumble-cut": {
|
|
type: "text-[12px] uppercase tracking-[0.18em]",
|
|
color: "hsl(196, 78%, 64%)",
|
|
family: FACE.terminal,
|
|
},
|
|
"room-gate": {
|
|
type: "text-[12px] uppercase tracking-[0.18em]",
|
|
color: "hsl(196, 78%, 64%)",
|
|
family: FACE.terminal,
|
|
},
|
|
"boom-tame": {
|
|
type: "text-[12px] uppercase tracking-[0.18em]",
|
|
color: "hsl(196, 78%, 64%)",
|
|
family: FACE.terminal,
|
|
},
|
|
"harsh-tame": {
|
|
type: "text-[12px] uppercase tracking-[0.18em]",
|
|
color: "hsl(196, 78%, 64%)",
|
|
family: FACE.terminal,
|
|
},
|
|
|
|
// --- character: the costumes. This is where type does the work ------------
|
|
// A phone's band is narrow; so is the tracking, on a face with no warmth.
|
|
telephone: {
|
|
type: "text-[13px] uppercase tracking-[0.3em]",
|
|
color: "hsl(186, 85%, 62%)",
|
|
family: FACE.terminal,
|
|
},
|
|
// A dial face: high-contrast serif caps, spaced like printed frequencies.
|
|
"radio-am": {
|
|
type: "text-[14px] uppercase tracking-[0.24em]",
|
|
color: "hsl(42, 92%, 62%)",
|
|
family: FACE.editorial,
|
|
},
|
|
// Shouted through a horn — the heaviest, narrowest thing available, leaning.
|
|
megaphone: {
|
|
type: "text-[17px] font-black italic uppercase tracking-tight",
|
|
color: "hsl(12, 90%, 64%)",
|
|
family: FACE.condensed,
|
|
},
|
|
// Struck on a machine, played back years later.
|
|
"lofi-tape": {
|
|
type: "text-[13px] tracking-wide",
|
|
color: "hsl(28, 72%, 62%)",
|
|
family: FACE.typewriter,
|
|
},
|
|
// Bolted to a wall in a station concourse.
|
|
"pa-system": {
|
|
type: "text-[13px] uppercase tracking-[0.26em]",
|
|
color: "hsl(222, 80%, 70%)",
|
|
family: FACE.engraved,
|
|
},
|
|
// Small, squeezed through a grille, no room for anything but the letters.
|
|
intercom: {
|
|
type: "text-[12px] font-bold uppercase tracking-tighter",
|
|
color: "hsl(96, 68%, 60%)",
|
|
family: FACE.terminal,
|
|
},
|
|
// The name is a joke and the type is in on it.
|
|
"doofus-worble": {
|
|
type: "text-[16px] tracking-[0.1em]",
|
|
color: "hsl(288, 85%, 72%)",
|
|
family: FACE.theatrical,
|
|
},
|
|
// Small, fast and squeaky — tight and bright, nothing about it sits still.
|
|
chipmunk: {
|
|
type: "text-[12px] font-black uppercase tracking-tighter",
|
|
color: "hsl(54, 88%, 66%)",
|
|
family: FACE.geometric,
|
|
},
|
|
// Huge and slow — the biggest size here, set on a face bolted to a monument.
|
|
giant: {
|
|
type: "text-[18px] font-black tracking-[0.02em]",
|
|
color: "hsl(250, 72%, 68%)",
|
|
family: FACE.engraved,
|
|
},
|
|
// Rough and too close — heavy industrial caps, growled rather than shouted.
|
|
monster: {
|
|
type: "text-[15px] font-black uppercase tracking-[0.05em]",
|
|
color: "hsl(350, 78%, 64%)",
|
|
family: FACE.condensed,
|
|
},
|
|
|
|
// --- space: rooms. Light and wide, because that is what space looks like ---
|
|
"room-tight": {
|
|
type: "text-[13px] uppercase tracking-[0.2em]",
|
|
color: "hsl(252, 74%, 72%)",
|
|
family: FACE.geometric,
|
|
},
|
|
"room-natural": {
|
|
type: "text-[13px] uppercase tracking-[0.26em]",
|
|
color: "hsl(258, 78%, 72%)",
|
|
family: FACE.geometric,
|
|
},
|
|
// The biggest room gets the widest setting — the word itself opens out.
|
|
hall: {
|
|
type: "text-[15px] uppercase tracking-[0.4em]",
|
|
color: "hsl(246, 84%, 74%)",
|
|
family: FACE.engraved,
|
|
},
|
|
"slap-echo": {
|
|
type: "text-[13px] uppercase tracking-[0.28em]",
|
|
color: "hsl(274, 76%, 70%)",
|
|
family: FACE.geometric,
|
|
},
|
|
"dub-throw": {
|
|
type: "text-[14px] italic tracking-[0.3em]",
|
|
color: "hsl(312, 78%, 70%)",
|
|
family: FACE.editorial,
|
|
},
|
|
};
|
|
|
|
export function fxPresetStyle(presetId: string): FxPresetStyle {
|
|
return FX_PRESET_STYLE[presetId] ?? FX_PRESET_STYLE_DEFAULT;
|
|
}
|
|
|
|
/**
|
|
* The wash behind a titled module, derived from the colour of its own title.
|
|
*
|
|
* Derived rather than picked: twenty hand-chosen pairs is twenty chances for
|
|
* one to clash with its own title, and a hue rotation cannot. Same hue,
|
|
* saturation pulled right down and lightness taken to near-black, so the panel
|
|
* reads as tinted rather than coloured — the rack sits on `#0C0C0E` and
|
|
* anything with real lightness here would fight every control on top of it.
|
|
*
|
|
* Takes a colour rather than an id so the smart modules can use it too: the
|
|
* carve has a title worth setting and no preset entry to look up. Returns null
|
|
* for a colour it cannot read, which is how a caller opts out.
|
|
*/
|
|
export function fxTintWash(color: string): string | null {
|
|
// Decimals allowed: the preset colours are whole numbers, but `fxFamilyTint`
|
|
// computes its lightness and emits `62.0%`, which an integer-only pattern
|
|
// silently declines — the module then renders with no wash at all.
|
|
const num = String.raw`\d+(?:\.\d+)?`;
|
|
const hsl = new RegExp(`hsl\\(\\s*(${num}),\\s*${num}%,\\s*${num}%\\s*\\)`).exec(color);
|
|
if (!hsl) return null;
|
|
// 22% saturation at 11% lightness: present enough to tell two brackets apart
|
|
// at a glance, dark enough that white body text still clears WCAG AA on it.
|
|
return `hsl(${hsl[1]}, 22%, 11%)`;
|
|
}
|
|
|
|
/** The wash behind a preset's bracket. Null when the preset has no character. */
|
|
export function fxPresetBackground(presetId: string): string | null {
|
|
const style = FX_PRESET_STYLE[presetId];
|
|
return style ? fxTintWash(style.color) : null;
|
|
}
|