diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml
index 6864551ef7..ce10956fca 100644
--- a/samples/ControlCatalog/Pages/AcrylicPage.xaml
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml
@@ -6,21 +6,21 @@
x:Class="ControlCatalog.Pages.AcrylicPage">
-
-
+
+
@@ -30,7 +30,17 @@
+
+
+
+
+
+
@@ -42,7 +52,17 @@
+
+
+
+
+
+
@@ -52,7 +72,7 @@
diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs
index 3c093b1919..2250e175b1 100644
--- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs
+++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs
@@ -1,4 +1,6 @@
-namespace Avalonia.Media
+using System;
+
+namespace Avalonia.Media
{
public class ExperimentalAcrylicBrush : Brush, IExperimentalAcrylicBrush
{
@@ -10,7 +12,7 @@
TintOpacityProperty,
TintLuminosityOpacityProperty);
}
-
+
///
/// Defines the property.
///
@@ -53,7 +55,7 @@
set => SetValue(FallbackColorProperty, value);
}
- public double TintLuminosityOpacity
+ public double? TintLuminosityOpacity
{
get => GetValue(TintLuminosityOpacityProperty);
set => SetValue(TintLuminosityOpacityProperty, value);
@@ -63,5 +65,204 @@
{
return new ImmutableExperimentalAcrylicBrush(this);
}
+
+ public struct HsvColor
+ {
+ public float Hue { get; set; }
+ public float Saturation { get; set; }
+ public float Value { get; set; }
+ }
+
+ public static Color FromHsv(HsvColor color)
+ {
+ var i = (float)Math.Floor(color.Hue * 6f);
+ var f = color.Hue * 6f - i;
+ var p = color.Value * (1f - color.Saturation);
+ var q = color.Value * (1f - f * color.Saturation);
+ var t = color.Value * (1f - (1f - f) * color.Saturation);
+
+ switch (i % 6)
+ {
+ case 0:
+ return new Color(255, (byte)(255.0 * color.Value), (byte)(255.0 * t), (byte)(255.0 * p));
+ case 1:
+ return new Color(255, (byte)(255.0 * q), (byte)(255.0 * color.Value), (byte)(255.0 * p));
+ case 2:
+ return new Color(255, (byte)(255.0 * p), (byte)(255.0 * color.Value), (byte)(255.0 * t));
+ case 3:
+ return new Color(255, (byte)(255.0 * p), (byte)(255.0 * q), (byte)(255.0 * color.Value));
+ case 4:
+ return new Color(255, (byte)(255.0 * t), (byte)(255.0 * p), (byte)(255.0 * color.Value));
+ default:
+ case 5:
+ return new Color(255, (byte)(255.0 * color.Value), (byte)(255.0 * p), (byte)(255.0 * q));
+ }
+ }
+
+ public static HsvColor RgbToHsv(Color color)
+ {
+ var r = color.R /255.0f;
+ var g = color.G / 255.0f;
+ var b = color.B / 255.0f;
+ var max = Math.Max(r, Math.Max(g, b));
+ var min = Math.Min(r, Math.Min(g, b));
+
+ float h, s, v;
+ h = s = v = max;
+
+ //v = (0.299f * r + 0.587f * g + 0.114f * b);
+
+ var d = max - min;
+ s = max == 0 ? 0 : d / max;
+
+ if (max == min)
+ {
+ h = 0; // achromatic
+ }
+ else
+ {
+ if (max == r)
+ {
+ h = (g - b) / d + (g < b ? 6 : 0);
+ }
+ else if (max == g)
+ {
+ h = (b - r) / d + 2;
+ }
+ else if (max == b)
+ {
+ h = (r - g) / d + 4;
+ }
+
+ h /= 6;
+ }
+
+ return new HsvColor { Hue = h, Saturation = s, Value = v };
+ }
+
+ public Color GetLuminosityColor ()
+ {
+ return GetLuminosityColor(TintColor, TintLuminosityOpacity);
+ }
+
+ Color GetLuminosityColor(Color tintColor, double? luminosityOpacity)
+ {
+ var rgbTintColor = tintColor;
+
+ // If luminosity opacity is specified, just use the values as is
+ if (luminosityOpacity.HasValue)
+ {
+ return new Color((byte)(255.0 * Math.Min(1.0, Math.Max(0.0, luminosityOpacity.Value))), tintColor.R, tintColor.G, tintColor.B);
+ }
+ else
+ {
+ // 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 double minHsvV = 0.125;
+ const double maxHsvV = 0.965;
+
+ var hsvTintColor = RgbToHsv(rgbTintColor);
+
+ var clampedHsvV = Math.Max(Math.Min(hsvTintColor.Value, minHsvV), maxHsvV);
+ var hsvLuminosityColor = hsvTintColor;
+ hsvLuminosityColor.Value = (float)clampedHsvV;
+
+ var rgbLuminosityColor = FromHsv(hsvLuminosityColor);
+
+ // Now figure out luminosity opacity
+ // Map original *tint* opacity to this range
+ const double minLuminosityOpacity = 0.15;
+ const double maxLuminosityOpacity = 1.03;
+
+ double luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity;
+ double mappedTintOpacity = ((tintColor.A / 255.0) * luminosityOpacityRangeMax) + minLuminosityOpacity;
+
+ // Finally, combine the luminosity opacity and the HsvV-clamped tint color
+ return new Color((byte)(255.0 * Math.Min(mappedTintOpacity, 1.0)), rgbLuminosityColor.R, rgbLuminosityColor.G, rgbLuminosityColor.B);
+ }
+
+ }
+
+
+ public Color GetEffectiveTintColor()
+ {
+ var tintColor = TintColor;
+ double tintOpacity = TintOpacity;
+
+ // Update tintColor's alpha with the combined opacity value
+ // If LuminosityOpacity was specified, we don't intervene into users parameters
+ if (false)//TintLuminosityOpacity() != nullptr)
+ {
+ //tintColor.A = static_cast(round(tintColor.A * tintOpacity));
+ }
+ else
+ {
+ double tintOpacityModifier = GetTintOpacityModifier(tintColor);
+
+ tintColor = new Color((byte)(Math.Round(tintColor.A * tintOpacity * tintOpacityModifier)), tintColor.R, tintColor.G, tintColor.B);
+ }
+
+ return tintColor;
+ }
+
+ double GetTintOpacityModifier(Color 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 double midPoint = 0.50; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning.
+
+ const double whiteMaxOpacity = 0.45; // 100% luminosity
+ const double midPointMaxOpacity = 0.90; // 50% luminosity
+ const double blackMaxOpacity = 0.85; // 0% luminosity
+
+ var hsv = RgbToHsv(tintColor);
+
+ if(tintColor == Colors.Red)
+ {
+
+ }
+
+ double opacityModifier = midPointMaxOpacity;
+
+ if (hsv.Value != midPoint)
+ {
+ // Determine maximum suppression amount
+ double lowestMaxOpacity = midPointMaxOpacity;
+ double maxDeviation = midPoint;
+
+ if (hsv.Value > midPoint)
+ {
+ lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV)
+ maxDeviation = 1 - maxDeviation;
+ }
+ else if (hsv.Value < midPoint)
+ {
+ lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV)
+ }
+
+ double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity;
+
+ // Determine normalized deviation from the midpoint
+ double deviation = Math.Abs(hsv.Value - midPoint);
+ double normalizedDeviation = deviation / maxDeviation;
+
+ // If we have saturation, reduce opacity suppression to allow that color to come through more
+ if (hsv.Saturation > 0)
+ {
+ // Dampen opacity suppression based on how much saturation there is
+ maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0);
+ }
+
+ double opacitySuppression = maxOpacitySuppression * normalizedDeviation;
+
+ opacityModifier = midPointMaxOpacity - opacitySuppression;
+ }
+
+ return opacityModifier;
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs
index adc9335eab..af7bf02e15 100644
--- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs
+++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs
@@ -1,15 +1,21 @@
-namespace Avalonia.Media
+using System.Drawing;
+
+namespace Avalonia.Media
{
public interface IExperimentalAcrylicBrush : IBrush
{
AcrylicBackgroundSource BackgroundSource { get; }
- Color TintColor { get; }
+ Color TintColor { get; }
double TintOpacity { get; }
- double TintLuminosityOpacity { get; }
+ double? TintLuminosityOpacity { get; }
Color FallbackColor { get; }
+
+ Color GetEffectiveTintColor();
+
+ Color GetLuminosityColor();
}
}
diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs
index ad91b5fa42..67a94c4fb3 100644
--- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs
+++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs
@@ -4,10 +4,13 @@ namespace Avalonia.Media
{
public readonly struct ImmutableExperimentalAcrylicBrush : IExperimentalAcrylicBrush, IEquatable
{
+ private readonly Color luminosityColor;
+
public ImmutableExperimentalAcrylicBrush(IExperimentalAcrylicBrush brush)
{
+ luminosityColor = brush.GetLuminosityColor();
BackgroundSource = brush.BackgroundSource;
- TintColor = brush.TintColor;
+ TintColor = brush.GetEffectiveTintColor();
TintOpacity = brush.TintOpacity;
TintLuminosityOpacity = brush.TintLuminosityOpacity;
FallbackColor = brush.FallbackColor;
@@ -20,7 +23,7 @@ namespace Avalonia.Media
public double TintOpacity { get; }
- public double TintLuminosityOpacity { get; }
+ public double? TintLuminosityOpacity { get; }
public Color FallbackColor { get; }
@@ -43,6 +46,11 @@ namespace Avalonia.Media
return obj is ImmutableExperimentalAcrylicBrush other && Equals(other);
}
+ public Color GetEffectiveTintColor()
+ {
+ return TintColor;
+ }
+
public override int GetHashCode()
{
unchecked
@@ -60,6 +68,11 @@ namespace Avalonia.Media
}
}
+ public Color GetLuminosityColor()
+ {
+ return luminosityColor;
+ }
+
public static bool operator ==(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right)
{
return left.Equals(right);
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index b43e20e390..ec3c254a4d 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -713,8 +713,10 @@ namespace Avalonia.Skia
var noiseOpcity = 0.09 * brush.Opacity;
- var excl = new SKColor(255, 255, 255, (byte)(255 * acrylicBrush.TintLuminosityOpacity));
- var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * (tintOpacity * acrylicBrush.Opacity * (acrylicBrush.TintColor.A / 255.0))));
+ var tintColor = acrylicBrush.GetEffectiveTintColor();
+ var luminosityColor = acrylicBrush.GetLuminosityColor();
+ var excl = new SKColor(luminosityColor.R, luminosityColor.G, luminosityColor.B, (byte)(255* (luminosityColor.A /255.0) * 0.1));
+ var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A /255.0) * acrylicBrush.Opacity)));
tint = SimpleColorBurn(excl, tint);