Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the theme builder open source #350

Open
isochronous opened this issue Oct 24, 2024 · 4 comments
Open

Make the theme builder open source #350

isochronous opened this issue Oct 24, 2024 · 4 comments

Comments

@isochronous
Copy link

isochronous commented Oct 24, 2024

What the title said. Here's my situation.

We have 200+ clients, all with their own vanity themes. The themes are currently defined in some .scss files, like so:

$primary-color: #00FF00;
$accent-color:#FF0000;
$tertiary-color: #0000FF;

@include theme-roller($primary-color, $accent-color, $tertiary-color);

Where theme-roller is obviously a mixin that does the actual theme generation with @angular/material mixins.

Well, we'd like to use these .scss files to programmatically generate a material 3 theme for each customer. And, amazingly, as far as I've been able to find, there's currently no way to do this programmatically with the crucial (for our customers) "Exact color match" option. The only tool I know of for generating themes programmatically (the @angular/material "m3-theme" schematic) does not offer said option. And even more amazingly, there's no source code that I can find on how this tool implements that option, so I can't even modify the m3-theme schematic to include it myself.

So, yeah, publishing some source code would be really helpful for those of us who would like to know how this tool is doing what it's doing.

@Harm-Nullix
Copy link

This should get some more heat.

@chainlift
Copy link

What the title said. Here's my situation.

We have 200+ clients, all with their own vanity themes. The themes are currently defined in some .scss files, like so:

$primary-color: #00FF00;
$accent-color:#FF0000;
$tertiary-color: #0000FF;

@include theme-roller($primary-color, $accent-color, $tertiary-color);

Where theme-roller is obviously a mixin that does the actual theme generation with @angular/material mixins.

Well, we'd like to use these .scss files to programmatically generate a material 3 theme for each customer. And, amazingly, as far as I've been able to find, there's currently no way to do this programmatically with the crucial (for our customers) "Exact color match" option. The only tool I know of for generating themes programmatically (the @angular/material "m3-theme" schematic) does not offer said option. And even more amazingly, there's no source code that I can find on how this tool implements that option, so I can't even modify the m3-theme schematic to include it myself.

So, yeah, publishing some source code would be really helpful for those of us who would like to know how this tool is doing what it's doing.

It's already open source, actually. Unless I'm misunderstanding your question.

The actual generator algorithm is available at material-foundation/material-color-utilities.

@Harm-Nullix
Copy link

You can get a basic theme using that package, and yes that is used, but the theme builder does do a lot of logic (it seems) that is different then the basic usage of the package. I myself have been fiddling to reproduce the exact theme for a year without success

@chainlift
Copy link

chainlift commented Nov 19, 2024

You can get a basic theme using that package, and yes that is used, but the theme builder does do a lot of logic (it seems) that is different then the basic usage of the package. I myself have been fiddling to reproduce the exact theme for a year without success

I've encountered a similar problem. To be perfectly honest, I decided to give up on reverse engineering the exact tone values the theme generator pulls, because you're right; there are inconsistencies. For example, the Figma theme builder forbids users from truly setting chroma to 0. If you attempt, it'll reset to ~2 after you close out. And m3 apps produced by Google directly always seem a bit more vibrant than those created from the public tools.

I suspect the actual logic of the theme generator relies on a few overrides and niche rule exceptions that aren't publicized or baked into the publicly-accessible theme builders.

Anyway, you've mentioned "basic usage." Are you referring to calling the themeFromSourceColor or themeFromImage methods? If so, try this solution. It lets you pass in your key colors, generate tones for each from 1 to 99 (100 is always pure white and 0 is pure black so i don't bother including them, and the tones are really all you need because the 'perceptually accurate' tone scale is what really makes the system unique).

(All I know is JS/TS btw so apologies if this isn't helpful for the platform you're building with)

1. I start with an object containing my key colors.

I like to do it with my custom colors already baked in. Here's an example of doing it with state in React, but it can be a normal const variable too.

const [palette, setPalette] = useState({
    primary: '#035eff',
    secondary: '#badcff',
    tertiary: '#00ddfe',
    neutral: '#000107',
    neutralvariant: '#3f4f5b',
    error: '#dd305c',
    warning: '#feb600',
    success: '#0cfecd',
    info: '#175bfc',
  });

2. Then, I use this function to generate tones from 1-99 for every key color and save them as camelCased key:value pairs.

const updateTheme = useCallback(async (palette) => {

    class TonalSwatches extends TonalPalette {
      constructor(hue, chroma) {
        super(hue, chroma);
        var swatch = TonalPalette.fromHueAndChroma(hue, chroma);

        for (let i = 1; i <= 99; i++) {
          this[`_${i}`] = hexFromArgb(swatch.tone(i));
        }
      }
    }

    Object.keys(palette).forEach((key) => {

      var argb = argbFromHex(palette[key]);
      var hct = Hct.fromInt(argb);

      var tones = new TonalSwatches(hct.hue, hct.chroma);

      // map the tones from each color group to a swatch name

      switch (key) {
        case 'neutral':
          setTheme(prevTheme => ({
            ...prevTheme,

            light: {
              ...prevTheme.light,
              background: tones._99,
              onBackground: tones._10,
              surfaceDim: tones._87,
              surface: tones._98,
              surfaceBright: tones._98,
              surfaceContainerLowest: 'white',
              surfaceContainerLow: tones._96,
              surfaceContainer: tones._94,
              surfaceContainerHigh: tones._92,
              surfaceContainerHighest: tones._90,
              onSurface: tones._10,
              inverseSurface: tones._20,
              inverseOnSurface: tones._95
            },
            dark: {
              ...prevTheme.dark,
              background: tones._10,
              onBackground: tones._85, /** Let's just deal with background and onbackground for now. */
              /**
              NOTE: In m3, surface containers from lowest to highest in dark mode go from darkest to brightest. In our system, LiftKit, they go from brightest to darkest.
              This is because the default dark backgrounds and surfaces are a little too black and cause halation effects
              for people with astigmatism, which includes me as well as a sizeable portion of our userbase (people who stare at screens all day) */

              surfaceContainerLowest: tones._12,
              surfaceDim: tones._14,
              surface: tones._15,
              surfaceContainerLow: tones._17,
              surfaceContainer: tones._20,
              surfaceContainerHigh: tones._24,
              surfaceContainerHighest: tones._28,
              surfaceBright: tones._30,
              onSurface: tones._90,
              inverseSurface: tones._98,
              inverseOnSurface: tones._10,

            }
          }));
          break;
        case 'neutralvariant':
          setTheme(prevTheme => ({
            ...prevTheme,

            light: {
              ...prevTheme.light,
              surfaceVariant: tones._80,
              onSurfaceVariant: tones._30,
              outline: tones._90,
              outlineVariant: tones._80,
            },

            dark: {
              ...prevTheme.dark,
              surfaceVariant: tones._20,
              onSurfaceVariant: tones._70,
              outline: tones._50,
              outlineVariant: tones._30,
            }

          }
          ));
          break;
        case 'primary':
          setTheme(prevTheme => ({
            ...prevTheme,

            light: {
              ...prevTheme.light,
              [key]: tones._40,
              [`on${toSentenceCase(key)}`]: tones._98,
              [`${key}Container`]: tones._90,
              [`on${toSentenceCase(key)}Container`]: tones._10,
              [`${(key)}Fixed`]: tones._90,
              [`${(key)}FixedDim`]: tones._80,
              [`on${toSentenceCase(key)}Fixed`]: tones._10,
              [`on${toSentenceCase(key)}FixedVariant`]: tones._30,
              ['inversePrimary']: tones._80
            },
            dark: {
              ...prevTheme.dark,
              [key]: tones._80,
              [`on${toSentenceCase(key)}`]: tones._20,
              [`${key}Container`]: tones._30,
              [`on${toSentenceCase(key)}Container`]: tones._90,
              [`${(key)}Fixed`]: tones._90,
              [`${(key)}FixedDim`]: tones._80,
              [`on${toSentenceCase(key)}Fixed`]: tones._10,
              [`on${toSentenceCase(key)}FixedVariant`]: tones._30,
              ['inversePrimary']: tones._80
            },


          }));
          break;
        case 'secondary':
        case 'tertiary':
          setTheme(prevTheme => ({
            ...prevTheme,
            light: {
              ...prevTheme.light,
              [key]: tones._40,
              [`on${toSentenceCase(key)}`]: tones._98,
              [`${key}Container`]: tones._90,
              [`on${toSentenceCase(key)}Container`]: tones._10,
              [`${(key)}Fixed`]: tones._90,
              [`${(key)}FixedDim`]: tones._80,
              [`on${toSentenceCase(key)}Fixed`]: tones._10,
              [`on${toSentenceCase(key)}FixedVariant`]: tones._30,
            },
            dark: {
              ...prevTheme.dark,
              [key]: tones._80,
              [`on${toSentenceCase(key)}`]: tones._20,
              [`${key}Container`]: tones._30,
              [`on${toSentenceCase(key)}Container`]: tones._90,
              [`${(key)}Fixed`]: tones._90,
              [`${(key)}FixedDim`]: tones._80,
              [`on${toSentenceCase(key)}Fixed`]: tones._10,
              [`on${toSentenceCase(key)}FixedVariant`]: tones._30,
            },
          }));
        default:
          setTheme(prevTheme => ({
            ...prevTheme,
            light: {
              ...prevTheme.light,
              [key]: tones._40,
              [`on${toSentenceCase(key)}`]: tones._98,
              [`${key}Container`]: tones._90,
              [`on${toSentenceCase(key)}Container`]: tones._10,

            },
            dark: {
              ...prevTheme.dark,
              [key]: tones._80,
              [`on${toSentenceCase(key)}`]: tones._20,
              [`${key}Container`]: tones._30,
              [`on${toSentenceCase(key)}Container`]: tones._90,
            },
          }));
      }
    });
  }, []);

3. How this solved the problem for me:

You'll notice in my neutrals, especially, that the values for surfaces do not line up 1:1 with the swatches the theme builder says it uses. That's where you can make tweaks fast, by modifying the tones. Since you have multiple vanity themes, as long as each one's in its own repo, you can fine-tune it on a case-by-case basis. In the example below, I have my dark mode surfaceContainers significantly brighter than the defaults.

Does this help at all?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants