mirror of
https://github.com/hanzoai/font.git
synced 2026-11-02 23:42:52 +00:00
The presets carried what they were fitted against — a licensed text face, a licensed display one, and the residuals of each search. That is our own working metadata, not the licence, and it is not what a preset IS: book is the reading weight, medium the step up before display, wide is widened and weighted to hold the widening. Each now says that and nothing else, and the numbers a search produced stay out of the shipped data, the stylesheet comments, the types and the crate. A preset also stopped naming another brand's surface. One typeface serves several brands, and a page for one of them should read as though the others do not exist — so the five are described by what the setting does, and the page says which of them that brand uses. The prose was still the upstream project's: it credited another company's design team, pointed downloads at their releases, told you to `npm install zen`, and listed seven other foundries' typefaces under Inspiration. Every import path in font.d.ts declared module "zen/font", which is not this package's name, so the types matched nothing a consumer could write. LICENSE.txt is untouched. The copyright notice travels with each copy, and that is where a licence belongs.
138 lines
5.5 KiB
JavaScript
138 lines
5.5 KiB
JavaScript
/* The Zen presets, defined once and emitted four ways.
|
|
*
|
|
* Each is a point in Zen's own parameter space that a surface has settled on, so
|
|
* it names the voice ("wide") rather than restating three numbers that then drift
|
|
* between the stylesheet and the component. The same five reach CSS, JS, the
|
|
* TypeScript types and the Rust crate from this file.
|
|
*
|
|
* Run: node scripts/presets.mjs (writes dist/presets.{css,js,d.ts} and crates/zen/src/presets.rs)
|
|
*/
|
|
import { writeFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { dirname, join } from 'node:path'
|
|
|
|
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist')
|
|
|
|
export const PRESETS = {
|
|
air: {
|
|
wght: 220, scaleX: 1, track: -0.03,
|
|
note: 'Thin display. Set large, tracked in.',
|
|
},
|
|
book: {
|
|
wght: 497, scaleX: 1, track: 0,
|
|
note: 'Text. The reading weight.',
|
|
},
|
|
medium: {
|
|
wght: 606, scaleX: 1, track: 0,
|
|
note: 'UI emphasis. A step up from book without becoming display.',
|
|
},
|
|
wide: {
|
|
// Widening is horizontal, so it reaches the uprights and leaves the
|
|
// horizontals where they were — which is why this carries a weight as well
|
|
// as a scale. Weight alone cannot widen; scale alone comes out light.
|
|
wght: 650, scaleX: 1.4861, track: -0.095,
|
|
note: 'Monumental caps. Widened, and weighted to hold the widening.',
|
|
},
|
|
round: {
|
|
wght: 900, scaleX: 1, track: -0.018,
|
|
note: 'Heavy and friendly at large sizes.',
|
|
},
|
|
}
|
|
|
|
/* A surface MIGRATING off a previous text face multiplies its font-size by this;
|
|
a surface simply using Zen does not. It is deliberately not baked into
|
|
`book`/`medium` — the preset is a voice, this is a migration correction, and
|
|
braiding them would apply it to type that was always Zen.
|
|
|
|
DIVIDE BY THE EM, NOT THE CAP, and that is the whole subtlety. `font-size` and
|
|
`size-adjust` both scale the em, so matching x-heights means matching
|
|
x-per-em, and a ratio of cap heights answers a question no CSS property asks. */
|
|
export const XHEIGHT_FACTOR = 0.9321
|
|
|
|
const decl = (p) => {
|
|
const out = [`font-variation-settings:'wght' ${p.wght}`]
|
|
if (p.track) out.push(`letter-spacing:${p.track}em`)
|
|
if (p.scaleX !== 1) out.push(
|
|
`transform:scaleX(${p.scaleX})`, 'transform-origin:left center', 'display:inline-block')
|
|
return out.join(';')
|
|
}
|
|
|
|
const css = `/* Zen presets — generated by scripts/presets.mjs. Do not edit.
|
|
*
|
|
* import '@hanzo/font/css' the faces
|
|
* import '@hanzo/font/presets.css' these
|
|
*
|
|
* A preset sets weight, and where the voice needs it, tracking and a horizontal
|
|
* scale. Anything that scales is inline-block with a left origin, and its LAYOUT
|
|
* box stays the untransformed width — a transform does not reflow, so give
|
|
* .zen-wide room or clip its container.
|
|
*/
|
|
${Object.entries(PRESETS).map(([k, p]) =>
|
|
`\n/* ${p.note} */\n.zen-${k}{${decl(p)}}`).join('\n')}
|
|
|
|
:root{
|
|
${Object.entries(PRESETS).map(([k, p]) =>
|
|
` --zen-${k}-wght:${p.wght};` +
|
|
(p.track ? `--zen-${k}-track:${p.track}em;` : '') +
|
|
(p.scaleX !== 1 ? `--zen-${k}-scale-x:${p.scaleX};` : '')).join('\n')}
|
|
--zen-xheight-factor:${XHEIGHT_FACTOR};
|
|
}
|
|
`
|
|
|
|
const js = `// Generated by scripts/presets.mjs. Do not edit.
|
|
export const PRESETS = ${JSON.stringify(PRESETS, null, 2)}
|
|
export const XHEIGHT_FACTOR = ${XHEIGHT_FACTOR}
|
|
|
|
/** Style object for a preset — for a surface that styles in code, not CSS. */
|
|
export function preset(name) {
|
|
const p = PRESETS[name]
|
|
if (!p) throw new Error(\`no Zen preset "\${name}" — have \${Object.keys(PRESETS).join(', ')}\`)
|
|
return {
|
|
fontFamily: 'Zen',
|
|
fontVariationSettings: \`'wght' \${p.wght}\`,
|
|
...(p.track ? { letterSpacing: \`\${p.track}em\` } : {}),
|
|
...(p.scaleX !== 1
|
|
? { transform: \`scaleX(\${p.scaleX})\`, transformOrigin: 'left center', display: 'inline-block' }
|
|
: {}),
|
|
}
|
|
}
|
|
`
|
|
|
|
const dts = `export interface ZenPreset {
|
|
wght: number
|
|
scaleX: number
|
|
track: number
|
|
note: string
|
|
}
|
|
export declare const PRESETS: Record<'air' | 'book' | 'medium' | 'wide' | 'round', ZenPreset>
|
|
export declare const XHEIGHT_FACTOR: number
|
|
export declare function preset(name: keyof typeof PRESETS): Record<string, string | number>
|
|
`
|
|
|
|
/* The Rust crate needs these numbers too — the CLI prints them and the wasm
|
|
build serves them — and a second hand-maintained copy is a second thing to
|
|
forget. It is generated here rather than read at run time because the crate
|
|
compiles to wasm with no filesystem. */
|
|
/* A Rust float literal: 1 is not f32 there, 1.0 is. */
|
|
const f = (n) => (Number.isInteger(n) ? `${n}.0` : String(n))
|
|
|
|
const rs = `// Generated by packages/zen/scripts/presets.mjs. Do not edit.
|
|
use crate::Preset;
|
|
|
|
pub const PRESETS: [Preset; ${Object.keys(PRESETS).length}] = [
|
|
${Object.entries(PRESETS).map(([k, p]) =>
|
|
// Emit the number, not a rounding of it. `toFixed(2)` shipped the crate
|
|
// scale_x 1.49 while the CSS got 1.4861 — the same fact at two precisions,
|
|
// which is the whole failure this file exists to prevent. `f` keeps a whole
|
|
// number a float, since Rust will not read `1` as f32.
|
|
` Preset { name: ${JSON.stringify(k)}, wght: ${f(p.wght)}, ` +
|
|
`scale_x: ${f(p.scaleX)}, track: ${f(p.track)} },`).join('\n')}
|
|
];
|
|
`
|
|
writeFileSync(join(DIST, '..', '..', '..', 'crates', 'zen', 'src', 'presets.rs'), rs)
|
|
|
|
writeFileSync(join(DIST, 'presets.css'), css)
|
|
writeFileSync(join(DIST, 'presets.js'), js)
|
|
writeFileSync(join(DIST, 'presets.d.ts'), dts)
|
|
console.log('wrote dist/presets.{css,js,d.ts} and crates/zen/src/presets.rs —', Object.keys(PRESETS).join(' '))
|