rydmike / flex_seed_scheme

A more flexible version of Flutter's ColorScheme.fromSeed
Other
24 stars 2 forks source link

[Question] Color fidelity while still customizing tones #19

Closed BinaryQuantumSoul closed 6 days ago

BinaryQuantumSoul commented 1 week ago

Hello, for my app I need a way to keep the original color input as well as generate a text color for it. Right now I use the variant fidelity which works for primaryContainer though I'd like the text color onPrimaryContainer to be more white/black rather than tinted to see it a bit better. However that same variant produces desaturated primary color. I'd like to have something more vivid like the flextone vivid.

Basically I'd need someway to get

I couldn't find a way using the flextones and variant args to do what I am asking for. Do you have a suggestion?

BinaryQuantumSoul commented 6 days ago

That is my current solution but it doesn't solve the onPrimaryContainer being too tinted and primaryContainer being too bright. Also it's not optimal to call two times the fromSeeds function. Basically what I need is a behavior very similar to the default one but with more vivid colors and which doesn't change white/black to blue or red to orange.

ThemeData themeFromColor(Color color, Brightness brightness) {
  var fidelityScheme = SeedColorScheme.fromSeeds(
    primaryKey: color,
    brightness: brightness,
    variant: FlexSchemeVariant.fidelity,
  );
  var colorScheme = SeedColorScheme.fromSeeds(
    primaryKey: color,
    brightness: brightness,
    tones: FlexTones.vivid(brightness),
    primaryContainer: fidelityScheme.primaryContainer,
    onPrimaryContainer: fidelityScheme.onPrimaryContainer,
  );

  return ThemeData(
    colorScheme: colorScheme,
    useMaterial3: true,
    splashFactory: NoSplash.splashFactory,
  );
}
rydmike commented 6 days ago

Hi @BinaryQuantumSoul thanks for your question and cool that you are using FlexSeedScheme.

When using seeded ColorScheme regardless of if it is Flutter's ColorScheme.from seed or FlexSeedScheme's SeedColorScheme.fromSeeds, you often want to pin your seed, typically your "brand" color(s) to certain ColorScheme colors. This is as simple as just overriding the desired ColorScheme with the brand/color. You can do the same for its contrasting color.

However, with FlexSeedScheme you have more options. You can modify the tones it uses and get something that works when you change the source color(s) too.

An example:

// A "brand" color for the M3 ColorScheme, you can have different
// colors for each palette, in this demo we use the same color for all.
const Color brandColor = Color(0xFF1F36BD);

// You can override tones of any built-in FlexTones tone mapping.
FlexTones appTones(Brightness brightness) {
  final bool isLight = brightness == Brightness.light;
  return FlexTones.vivid(brightness)
      // If you want to override a tone mapping of any built-in
      // FlexTones, you can do it with a copy with. Here we make
      // contrast color for primaryContainer use tone 100 in light mode,
      // which always white in HCT (Hue Chroma Tone) color space and
      // tone 0 in dark mode, which is always black.
      .copyWith(
        onPrimaryContainerTone: isLight ? 100 : 0,
      )
      // Demo: This modifier will make all surface colors monochrome
      // shaded without any color tint, but only when isLight is true.
      .monochromeSurfaces(isLight)
      // There are many more tone modifiers available,
      // see the FlexTones class. Set flag in them below to true to
      // try them. Basically the tone modifiers below are
      // only convenience copyWith, with many props used, like the one
      // demoed above.
      //
      // The first one, if set to true, also makes contrast colors on ALL 
      // main colors use black and white only, no tint. The copy with 
      // above only did it for onPrimaryContainer. Check API docs for more info.
      .onMainsUseBW(false) 
      .onSurfacesUseBW(false)
      .expressiveOnContainer(false)
      .higherContrastFixed(false)
      .surfacesUseBW(false);
}

// Example App theme
ThemeData appTheme(Brightness brightness, bool useMaterial3) {
  final bool isLight = brightness == Brightness.light;
  final ColorScheme colorScheme = SeedColorScheme.fromSeeds(
    brightness: brightness,
    // You can if you want to, use the brand color as key for
    // all main palettes for single hue ColorScheme with
    // various shades and tones of only the brand color used.
    primaryKey: brandColor,
    secondaryKey: brandColor,
    tertiaryKey: brandColor,
    // Pin the brand color to the primary color in light and to
    // primaryContainer in dark mode, this usually works well if
    // the brand color has brightness that is dark and prefers a
    // light on color. If it is the reverse then pin them the
    // other way around.
    primary: isLight ? brandColor : null,
    primaryContainer: isLight ? null : brandColor,
  );

  return ThemeData(
    colorScheme: colorScheme,
    useMaterial3: useMaterial3,
    visualDensity: VisualDensity.standard,
    splashFactory: NoSplash.splashFactory,
  );
}

This gives us a light scheme that looks like this:

Screenshot 2024-10-15 at 2 48 52

And dark scheme:

Screenshot 2024-10-15 at 2 49 25

Which of the FlexTones you use in the above setup to generate your ColorScheme is just a matter of your design preferences. It will work equally well technically and color contrast wise with any of them using the above setup.

Hope this helps and at least gives you some pointers, or even better that it fully answers your questions 😄

By the way

If you want to try all the ways to generate a ColorScheme easily with your own colors, you can do so eg in the Themes Playground app.

I just released flex_color_scheme 8.0.0-dev.1 (Yes still a dev release until I have done all quality assurance on it, but no more changes planned to the stable release. So far everything seems to work well 😄 )

https://pub.dev/packages/flex_color_scheme/versions/8.0.0-dev.1

and here is its compatible Playground version:

https://rydmike.com/flexcolorscheme/themesplayground-dev/

With it you can try all the ways of generating seed ColorSchemes and using custom colors by copy pasting them when using the last scheme in the Playground, which is customizable.

Obviosuly, if all you want for your custom theme is just the ColorScheme, then definitely stick to just flex_seed_scheme. FlexColorScheme + Themes Playground is more for visually designing your entire ThemeData and just copy pasting the ThemeData config code to your app. Even if not using it, it is great for experimenting with ColorScheme generation and entire app theme design too.

Screenshot 2024-10-15 at 2 57 28
BinaryQuantumSoul commented 6 days ago

Hello, thank you for your thorough answer!

The isLight switching between primary and primaryContainer was what I was missing. I still have an issue: when I input a black or gray color, the other one will be blue and I'd prefer it to be a lighter gray instead. I guess it's related to one of your flextones modifiers, but I need to keep the tints in most cases (maybe I need a checke on the chroma dimension?)

Here's what I settled for:

ThemeData themeFromColor(Color color, Brightness brightness) {
  final bool isLight = brightness == Brightness.light;
  final bool isBright = color.computeLuminance() > 0.5;

  final FlexTones tones = FlexTones.vivid(brightness).copyWith(
    onPrimaryTone: isLight ? 95 : 20,
    onPrimaryContainerTone: isLight ? 30 : 80,
  );

  final ColorScheme colorScheme = SeedColorScheme.fromSeeds(
    brightness: brightness,
    tones: tones,
    primaryKey: color,
    secondaryKey: color,
    tertiaryKey: color,
    primary: isLight ^ isBright ? color : null,
    primaryContainer: isLight ^ isBright ? null : color,
  );

  return ThemeData(
    colorScheme: colorScheme,
    useMaterial3: true,
    visualDensity: VisualDensity.standard,
    splashFactory: NoSplash.splashFactory,
  );
}

I tested your playground and I must say it's very very compete and a bit overpowered haha. Though would have been useful to input my own theme code instead of just ability to copy it, so I could test the colorschemes there.

rydmike commented 6 days ago

Yes I know ad correct observation on the odd default behavior of monochrome seeds.

Oddly, getting a blueish tonal color palette that seeded ColorScheme colors are then tone mapped to, when using white color or monochrome grays as seed colors is actually "a feature" of the underlying algo based on Material Color Utilities (MCU), that also Flutter's ColorScheme.fromSeed uses. If you use a pure black color, you actually get a reddish tonal palette.

This is odd default behavior imo too and shown in the recording below:

https://github.com/user-attachments/assets/118dba99-ab81-447b-ba30-a8583bdd0506

It also shows how to get rid of it. It would not really be "flex" if it could not do that too, right! 😄

The SeedColorScheme.fromSeeds has respectMonochromeSeed parameter that you can set to true to get rid of this odd behavior. It should imo be on by default, but I did not want to break the backward API compatibility with its own older versions and also the only and default behavior in Flutter ColorScheme.fromSeed and even in MCU it is based on. So it is an opt-in behavior.

Usage example:

// A "brand" color for the M3 ColorScheme, you can have different
// colors for each palette, in this demo we use the same color for all.
const Color brandColor = Color(0xFF1F36BD);
const Color aMonochromeColor = Color(0xFF111111);

// You can override tones of any built-in FlexTones tone mapping.
FlexTones appTones(Brightness brightness) {
  final bool isLight = brightness == Brightness.light;
  return FlexTones.vivid(brightness)
      // If you want to override a tone mapping of any built-in
      // FlexTones, you can do it with a copy with. Here we make
      // contrast color for primaryContainer use tone 100 in light mode,
      // which always white in HCT (Hue Chroma Tone) color space and
      // tone 0 in dark mode, which is always black.
      .copyWith(
        onPrimaryContainerTone: isLight ? 100 : 0,
      )
      // Demo: This modifier will make all surface colors monochrome
      // shaded without any color tint, but only when isLight is true.
      .monochromeSurfaces(isLight)
      // There are many more tone modifiers available,
      // see the FlexTones class. Set flag in them below to true to
      // try them. Basically the tone modifiers below are
      // only convenience copyWith, with many props used, like the one
      // demoed above.
      .onMainsUseBW(false)
      .onSurfacesUseBW(false)
      .expressiveOnContainer(false)
      .higherContrastFixed(false)
      .surfacesUseBW(false);
}

// Example App theme
ThemeData appTheme(Brightness brightness, bool useMaterial3) {
  final bool isLight = brightness == Brightness.light;
  final ColorScheme colorScheme = SeedColorScheme.fromSeeds(
    brightness: brightness,
    // You can if you want use the brand color as key for
    // all main palettes for single hue ColorScheme with
    // various shades and tones of only the brand color used.
    primaryKey: brandColor,
    secondaryKey: brandColor,
    // Using a monochrome seed color for all tertiaries.
    tertiaryKey: aMonochromeColor,
    // Only works if respect monochrome seed is true.
    // Flutter SDK and MCU (Material Color Utilities) does not
    // let you do this, but you can with FSS, but it is OFF
    // by default to keep
    // backwards compatibility with itself and Flutter+MCU.
    respectMonochromeSeed: true,
    // Pin the brand color to the primary color in light and to
    // primaryContainer in dark mode, this usually works well if
    // the brand color has brightness that is dark and prefers a
    // light on color. If it is the reverse then pin them the
    // other way around.
    primary: isLight ? brandColor : null,
    primaryContainer: isLight ? null : brandColor,
  );

  return ThemeData(
    colorScheme: colorScheme,
    useMaterial3: useMaterial3,
    visualDensity: VisualDensity.standard,
    splashFactory: NoSplash.splashFactory,
  );
}

The flag only impacts results when the seed color is totally monochrome, so it can well be on for everything. I think when you use a monochrome seed color it produces results that are more in line with what is expected.

Screenshot 2024-10-15 at 13 32 07


API Docs?

Sure there are API property docs about this too, but sadly pub API docs make discoverability of doc comments on extension method parameters quite poor.

I think I will need to document all parameters of extension methods and also of all class factory parameters, in all my packages, on the extension method level and factory level. Same goes for all function parameters.

The auto API doc system does not generate separate linked doc for their parameter. It only does it for class member properties. I think there is an issue about this being discussed.

Anyway, here is what its API docs states for SeedColorScheme.fromSeeds:

    /// If true, when a seed color is monochrome, it is recognized as such and
    /// the chroma is set to 0 to respect that it has no chroma. This is then
    /// used in its conversion from Color or integer value to HCT space, so
    /// we get all greyscale tones.
    ///
    /// If not set to true, we get a "cyan" tonal palette for monochrome and
    /// white seed colors, while black, gives a "red" tonal palette.
    ///
    /// Defaults to `false` to keep the default behavior of the package and the
    /// Material-3 color system.
    ///
    /// Prefer setting it to `true` if you want to get
    /// greyscale palette tones for any given monochrome seed color.
    ///
    /// If [respectMonochromeSeed] is true, any given configured minimum
    /// chroma value is ignored for a monochrome seed colors, as the input has
    /// chroma 0 and its chroma will be set to zero regardless of the value
    /// of minimum chroma. Minimum chroma is always 0 when
    /// [respectMonochromeSeed] is used.
    final bool respectMonochromeSeed = false,

And by the way, found two typos in it when digging this out for you, plus found a small bug the pre-release of the Playground when using the respect monochrome seed in it, seems like it is not implemented in the Playground to the color picker's display of the shorter tonal palette, as it is should be. So I got some bonus finds for his reply 😄

BinaryQuantumSoul commented 6 days ago

Thank you! And thanks for your quick response :) It finally looks good.

I just needed to set a threshold such that the monochrome fix is applied to all colors with chroma ≤ 5:

  final Hct hct = Hct.fromInt(color.value);
  if (hct.chroma <= 5) {
    color = Color(Hct.from(hct.hue, 0, hct.tone).toInt());
  }

  final ColorScheme colorScheme = SeedColorScheme.fromSeeds(
    brightness: brightness,
    tones: tones,
    respectMonochromeSeed: true, // fix monochrome
    primaryKey: color,
    secondaryKey: color,
    tertiaryKey: color,
    primary: isLight ^ isBright ? color : null,
    primaryContainer: isLight ^ isBright ? null : color,
  );