Microsoft-ui-xaml: How does WinUI create the color pallette(?) for a given (accent) color and color mode (dark/light)?

Created on 7 May 2020  Â·  36Comments  Â·  Source: microsoft/microsoft-ui-xaml

This noise PNG doesn't look like the noise texture actually used in acrylic. It has an alpha of value (possibly) 1, which means it is not the noise texture PNG used in the real AcrylicBrush.

Please update the noise texture to the correct PNG to remove any (potential) errors/threats, as it could be misleading.
Or at least make a README.md file that gives the correct alpha value for that PNG to be the same as the noise in acrylic.

question

All 36 comments

Acrylic's noise texture actually has an alpha of 100%, but when it is layered onto the surface, that layer is set to 2%

Oh, thanks!

@mdtauk I have a few more doubts.

  1. If the blur radius is 30.0f in C++, then how many px (pixels) is the blur radius (in CSS)? (I'm trying to learn to code in XAML and CS for UWPs and I would find it easier if I try it in CSS, as I know some CSS)
  2. What is the background-size of the noise PNG used? (A small one would make no difference and a big one will increase the size of the noise texture which would be of no use to us)

CSS is very different from XAML.

The Background Noise file I believe is either 256x256 but is tiled, so it repeats and is layed over the surface it is applied to.

The Blur would be 30px.

There are various over layers that make the Acrylic effect though.

image

This is how I do it for Mock Up designs
image

In Figma Microsoft uses a 60px blur, but all other samples say 30, and that is what is in the code too

  1. @mdtauk I know that the noise is tiled (because the noise image would be stretched or zoomed to fit if not tiled), but I don't know the scale value or the dimensions (other than 256x256) to which the noise is set (I think the noise PNG is resized to a smaller value because 256x256 doesn't look like the correct noise texture used in acrylic).

  2. At first, I too thought the blur would be 30px, but when I used

.acrylic {
  background-image: url("/Assets/Noise_5%.png");  /* 2% was invisible */
  background-size: round auto;  /* Tiled noise */
  backdrop-filter: blur(30px) brightness(90%) saturate(112%);
  /*
   * brightness = Tint White + Exclusion White ≡ Luminosity blend
   * saturate = Saturation (R|G|B) + 100% ≡ Tint/Color blend
   */
}

The background looked way more blurry than acrylic. So I tried blur(20px) instead and the result was somewhat closer:
CSS
delete1

Acrylic (Windows Terminal)
delete
And the fact that blur(20px) looked more accurate than blur(30px) got me confused.

  1. Another reason why I used 5% noise instead of 2% noise is this:
    Acrylic (Windows Terminal)
    delete_

CSS (Noise 2%)
delete_1

Notice the black area in this image. The noise PNG can be clearly seen in it. And in the second image, the noise can be barely seen and that too on the colored part only, meaning the tiled noise PNG is not a 256x256 image but something else, the same image with different dimensions. If the correct dimensions are known, we can resize the image and use an alpha of 2% for the image, and then it would look exactly the same in both the pictures.

DPI scaling may be a factor.

The Noise asset is from the SDK, so is direct from Microsoft.

Originally the Noise was set to 5% but in the next versions after, Microsoft changed this value and introduced Saturation Boost and Luminosity layers.

@SFM61319 The thing with XAML rendering is that there is no such unit like PX in CSS. All measurements are done in "Pixels" which then are converted into DPI pixels/PPI which are aware of factors such as display scaling and screen size. So on my device 40 "pixel" would be the same size as on a device with a smaller or larger screen, because those units get scaled to ensure that text is readable etc.

One XAML pixel does not have to correspond to one physical pixel (contrary to what CSS does).

Oh, thank you, @chingucoding! I have another question. Do you know like the conversion factor or value or the logic used in the saturation and brightness of Acrylic based on acrylicOpacity (i.e., the logic which uses 12% saturation and (80+10)% luminosity when acrylicOpacity is 0 and a different value, which keeps on increasing or decreasing by a factor/value, for when the acrylicOpacity is 1)?

I am sorry I was late, I didn't know you commented (I did not check my mail too).

There is something I was wondering @chingucoding
Windows has traditionally used 96 dpi as it's 100% scaling value, unlike Adobe and Apple which uses 72dpi. Does UWP Xaml use 96 or 72 (as the 100% scale factor)?

Hey @mdtauk, since you are online, can you please answer this question?

Hey @mdtauk, since you are online, can you please answer this question?

I thought I had. The Noise texture is tiled and set to a 2% opacity on top of the other layers that make up Acrylic.

I am not a Microsoft employee, so I don't have access to any source code for Acrylic Brush, so I can only tell you what I have read online, and what I have done for my design mockups.

Oh no, that was not my question, @mdtauk. I was talking about saturation and luminosity for different acrylic opacities.
It's ok if you don't know it, I just read you were not a Microsoft employee (I thought you were an employee the whole time).
Also, thank you for answering everything you knew about this :)

@SFM61319 The source code for AcrylicBrush is available online.

The logic for determining the luminosity is the following:

https://github.com/microsoft/microsoft-ui-xaml/blob/6381f8207a29b20bd417c8b7202926e3ccffe8be/dev/Materials/Acrylic/AcrylicBrush.cpp#L414-L463

All logic done to calculate the correct colors can should be in that page, albeit a bit nested or "hidden".

@mdtauk I don't know what the default DPI value for UWP XAML is. I think it might still be 96, though the question itsself confuses me a bit. Doesn't DPI rely on your monitor size?

@chingucoding I already read the source code online, and I did not quite understand some stuff.
I did understand that a value of 0 acrylic opacity is actually 0.125 and 1 is 0.965 (and not 1) (I don't know if I am correct). But what I did not understand is why the value of luminosityOpacityRangeMax particularly 0.88 (1.03 - 0.15) and how can I represent it in CSS (like brightness(88%) /* Different than this (at 90%) */ or brightness(12%) /* 100% - 88%; Too dark */)

Why some values are exactly the values they are is probably something @chigy or @kikisaints know.

Regarding the calculated luminosity, here is CSS pseudo code (assuming you have set aOpacity to the respective opacity (a value from 0 to 1) ):


--newOpacity : calc(calc(aOpacity * 0.88) + 0.15);

Oh, thanks, @chingucoding!

Luminosity was a recent addition, and I think it is used to solve the visibility of shadows within the Acrylic surfaces

I am so sorry for raising these many doubts but I have one more doubt, @chingucoding (This is another doubt I realized I didn't raise).
What/where is the variable for the luminosity blend and what equation is used to calculate the luminosity blend value? The one in the code:

double mappedTintOpacity = ((tintColor.A / 255.0) * 0.88) + 0.15;    // line: 457

gives you the tintOpacity but not the luminosity blend value. I believe that luminosity value increases with tintOpacity for light mode (or light colors) and decreases with tintOpacity for dark mode (or dark colors). I just don't know by what factor (as I didn't understand some part of the source code).

The value you are referring to in lin 457 is the opacity of the luminosity layer. The tintOpacity (the opacity of the tint layer) is being calculated here:

https://github.com/microsoft/microsoft-ui-xaml/blob/98a6b746dc409fa3c64bca4db2c3e6aa31e73470/dev/Materials/Acrylic/AcrylicBrush.cpp#L352-L412

@SFM61319 Are there any other questions regarding the Acrylic brush and its blur? Or can this issue be closed?

@SFM61319 Are there any other questions regarding the Acrylic brush and its blur? Or can this issue be closed?

Oh yea I'm so sorry I forgot I created this issue. Gonna close this!

I didnt want to open another issue so i am renaming and reopening this issue

How does WinUI create the color pallete (like themePrimaryColor which is what the user gives, themeSecondaryColor which is part of the pallette which is calculated by WinUI)?

and no this doesnt need triage, bot

like how does Fluent UI Theme Designer do that (too)?

@chingucoding uhh sorry for pinging you but this was the fastest way...

I don't know how the different shades for a given accent color is being generated, sorry. Maybe someone from the team knows more.

I think the Fluent Xaml Theme Editor is open source, so you could look at how they generate their shades. I am not sure WinUI 3.0 will contain the code, as it may remain a Windows OS feature - but it is possible WinUI 3.0 will contain the code to do it, for backwards compatible support?

I don't know how the different shades for a given accent color is being generated, sorry. Maybe someone from the team knows more.

oh, np.

  

I think the Fluent Xaml Theme Editor is open source, so you could look at how they generate their shades. I am not sure WinUI 3.0 will contain the code, as it may remain a Windows OS feature - but it is possible WinUI 3.0 will contain the code to do it, for backwards compatible support?

Yep I'll take a look at it, thanks!

Also, I am kinda confused. How is the tintColor in acrylic set again? Like i said a while ago, i was trying to implement acrylic and reveal brushes in CSS and JS to have a better understanding of how it all works. I wrote the exact same functions written in AcrylicBrush.cpp but in js, but i dont know how to get the final color, opacity, saturation and brightness for the acrylic. i will be pasting the functions i wrote in js just in case. thanks!

const clamp = (value=0.5, min=0, max=1) => {
  if (value < min)
    return min;

  if (value > max)
    return max;

  return value;
}

// ----------------------------------------------------------------------------------------------------------------------------------

const _getEffectiveTintColor = (tintColor, tintOpacity, tintLuminosityOpacity=null) => {
  let [r, g, b, a] = tintColor;

  a = a * tintOpacity;

  // Update tintColor's alpha with the combined opacity value
  // If LuminosityOpacity was specified, we don't intervene into users parameters
  if (tintLuminosityOpacity == null || tintLuminosityOpacity == undefined) {
    const tintOpacityModifier = _getTintOpacityModifier(tintColor);
    a = a * tintOpacityModifier;
  }

  return [r, g, b, a];
}


const _getTintOpacityModifier = (tintColor) => {
  /*
   * This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by 
   * compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure 
   * white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity).  The intensity of 
   * the effect increases linearly as luminosity deviates from 50%.  After this effect is calculated, we cancel it out
   * linearly as saturation increases from zero.
   */

  const midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning.

  const whiteMaxOpacity = 0.45; // 100% luminosity
  const midPointMaxOpacity = 0.9; // 50% luminosity
  const blackMaxOpacity = 0.85; // 0% luminosity

  const [h, s, v, a] = RGBAToHSVA(...tintColor);

  let opacityModifier = midPointMaxOpacity;

  if (v != midPoint) {
    // Determine maximum suppression amount
    let lowestMaxOpacity = midPointMaxOpacity;
    let maxDeviation = midPoint;

    if (v > midPoint)
      lowestMaxOpacity = whiteMaxOpacity, // At white (100% hsvV)
      maxDeviation = 1 - maxDeviation;

    else if (v < midPoint)
      lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV)

    let maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity;

    // Determine normalized deviation from the midpoint
    const deviation = Math.abs(v - midPoint);
    const normalizedDeviation = deviation / maxDeviation;

    // If we have saturation, reduce opacity suppression to allow that color to come through more
    if (s > 0)
      // Dampen opacity suppression based on how much saturation there is
      maxOpacitySuppression = maxOpacitySuppression * Math.max(1 - (s * 2), 0);

    const opacitySuppression = maxOpacitySuppression * normalizedDeviation;

    opacityModifier = midPointMaxOpacity - opacitySuppression;
  }

  return opacityModifier;
}


const _getEffectiveLuminosityColor = (tintColor, tintOpacity, tintLuminosityOpacity=null) => {
  let [r, g, b, a] = tintColor;

  a = a * tintOpacity;

  return _getLuminosityColor([r, g, b, a], tintLuminosityOpacity);
}


const _getLuminosityColor = (tintColor, luminosityOpacity=null) => {
  const [r, g, b, a] = tintColor;

  // If luminosity opacity is specified, just use the values as is
  if (luminosityOpacity != null && luminosityOpacity != undefined)
    return [r, g, b, clamp(luminosityOpacity)];

  // To create the Luminosity blend input color without luminosity opacity,
  // we're taking the TintColor input, converting to HSV, and clamping the V between these values
  const minHsvV = 0.125;
  const maxHsvV = 0.965;

  const [h, s, v, o] = RGBAToHSVA(r, g, b, a);
  const clampedHsvV = clamp(v, minHsvV, maxHsvV);
  const [lr, lg, lb, la] = HSVAToRGBA(h, s, clampedHsvV, o);

  // Now figure out luminosity opacity
  // Map original *tint* opacity to this range
  const minLuminosityOpacity = 0.15;
  const maxLuminosityOpacity = 1.03;

  const luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity;
  const mappedTintOpacity = (a * luminosityOpacityRangeMax) + minLuminosityOpacity;

  // Finally, combine the luminosity opacity and the HsvV-clamped tint color
  return [lr, lg, lb, Math.min(mappedTintOpacity, 1)];
}

what confuses me even more is there are two different functions for the colors (GetEffectiveTintColor and GetEffectiveLuminosityColor) and idk which is to be used for the final background color and which returns the filters like saturate and brightness along with blur (which we know to be ~20px)

I think the Fluent Xaml Theme Editor is open source, so you could look at how they generate their shades. I am not sure WinUI 3.0 will contain the code, as it may remain a Windows OS feature - but it is possible WinUI 3.0 will contain the code to do it, for backwards compatible support?

@mdtauk i could not find the editor's source which creates the themes based on the accent color passed (and the color mode - light/dark - iirc)...

AcrylicBrush is not just a simple color, it consists of multiple different layers that are stacked on top of each other:

Image showing recipe

So in order to properly implement that, you would need to be able to stack multiple different filters on top of each other.

@chingucoding yes I know that but in CSS you can use multiple (backdrop) filters in a single frame(?) by doing:

.acrylic {
  background-color: rgba(r, g, b, a);
  backdrop-filter: blur(20px) /* For the blur */ saturate(some_integer_or_double_for_saturation) brightness(some_integer_or_double_for_brightness);
}

and i wanted to know how to calculate the r, g, b, a values (idk which function of the above functions to use) for the background-color (passing an opaque color (i.e. alpha = 1) is returning an opaque color but thats not what AcrylicBrush does, it's minimum opacity is 0.125 and maximum is 0.965 (and not 1 so not opaque) iirc), the saturation level (some_integer_or_double_for_saturation) and the brightness (some_integer_or_double_for_brightness)

Both the tint and luminosity layer have colors and both of them are stacked on top of each other from what I can tell. If you don't want to use a luminosity layer, just use the _getEffectiveTintColor method.

Was this page helpful?
0 / 5 - 0 ratings