From 96f5ccb7f5b1344492e20ccd5a8e5560ffccf76e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 18:09:16 -0300 Subject: [PATCH] implementation of limiting opacity. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 34 ++- .../Media/ExperimentalAcrylicBrush.cs | 207 +++++++++++++++++- .../Media/IExperimentalAcrylicBrush.cs | 12 +- .../ImmutableExperimentalAcrylicBrush.cs | 17 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 6 +- 5 files changed, 259 insertions(+), 17 deletions(-) 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);