/** * The source of resources/node/scheme.mjs, which `php artisan material:scheme` runs. * * Rebuild with `npm run build:scheme` after changing this file or upgrading * @material/material-color-utilities; the bundle is committed so applications need * nothing but `node`. (The published library imports without file extensions, which * plain Node refuses, so it cannot be run unbundled anyway.) * * Input: one JSON argument — {seed, variant, spec, contrast, success, warning, info}. `spec` is * the colour spec, '2025' (M3 Expressive, the default) or '2021' (M3 as it first shipped). * Output: JSON on stdout — {seed, variant, spec, contrast, light: {role: hex}, dark: {role: hex}}. */ import { argbFromHex, customColor, hexFromArgb, Hct, MaterialDynamicColors, SchemeContent, SchemeExpressive, SchemeFidelity, SchemeFruitSalad, SchemeMonochrome, SchemeNeutral, SchemeRainbow, SchemeTonalSpot, SchemeVibrant, } from '@material/material-color-utilities' const VARIANTS = { 'tonal-spot': SchemeTonalSpot, vibrant: SchemeVibrant, expressive: SchemeExpressive, neutral: SchemeNeutral, fidelity: SchemeFidelity, content: SchemeContent, monochrome: SchemeMonochrome, rainbow: SchemeRainbow, 'fruit-salad': SchemeFruitSalad, } // Key colours seed the palettes; they are not roles a stylesheet paints with. const NOT_ROLES = /_palette_key_color$/ function fail(message) { process.stderr.write(`${message}\n`) process.exit(1) } let input try { input = JSON.parse(process.argv[2] ?? '') } catch { fail('Expected one JSON argument: {"seed": "#rrggbb", "variant": "tonal-spot", ...}') } const hex = /^#[0-9a-f]{6}$/i const Scheme = VARIANTS[input.variant] const SPECS = ['2021', '2025'] const spec = input.spec ?? '2025' if (!hex.test(input.seed ?? '')) fail(`The seed must be a #rrggbb colour, "${input.seed}" given.`) if (!Scheme) fail(`Unknown variant "${input.variant}". Use one of: ${Object.keys(VARIANTS).join(', ')}.`) if (!SPECS.includes(spec)) fail(`Unknown spec "${spec}". Use one of: ${SPECS.join(', ')}.`) for (const state of ['success', 'warning', 'info']) { if (!hex.test(input[state] ?? '')) fail(`The ${state} colour must be a #rrggbb colour, "${input[state]}" given.`) } const contrast = Number(input.contrast ?? 0) if (!(contrast >= -1 && contrast <= 1)) fail(`The contrast level must be between -1 and 1, "${input.contrast}" given.`) const source = Hct.fromInt(argbFromHex(input.seed)) const colors = new MaterialDynamicColors() function roles(isDark) { // The 2025 spec is M3 Expressive's colour; the library falls back to 2021 for the // variants the new spec does not define (fidelity, content, monochrome, …). 2021 is // M3's original colour, for an application whose palette was generated with it. const scheme = new Scheme(source, isDark, contrast, spec) const out = {} for (const color of colors.allColors) { if (color && !NOT_ROLES.test(color.name)) { out[color.name.replaceAll('_', '-')] = hexFromArgb(color.getArgb(scheme)) } } return { scheme, out } } const light = roles(false) const dark = roles(true) // success, warning and info are M3 custom colours, harmonisation off: harmonising pulls // each hue towards the seed, and a state has to stay recognisable whatever the brand is. for (const state of ['success', 'warning', 'info']) { const group = customColor(argbFromHex(input.seed), { name: state, value: argbFromHex(input[state]), blend: false }) for (const [theme, colours] of [['light', light.out], ['dark', dark.out]]) { colours[state] = hexFromArgb(group[theme].color) colours[`on-${state}`] = hexFromArgb(group[theme].onColor) colours[`${state}-container`] = hexFromArgb(group[theme].colorContainer) colours[`on-${state}-container`] = hexFromArgb(group[theme].onColorContainer) } } // A state colour drawn on the inverse surface (a snackbar's icon) is the other theme's. for (const state of ['error', 'success', 'warning', 'info']) { light.out[`inverse-${state}`] = dark.out[state] dark.out[`inverse-${state}`] = light.out[state] } process.stdout.write(JSON.stringify({ seed: input.seed.toLowerCase(), variant: input.variant, spec: light.scheme.specVersion, contrast, light: light.out, dark: dark.out, }))