A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

298 lines
12 KiB

using System;
using Avalonia.Reactive;
namespace Avalonia.Media
{
public class ExperimentalAcrylicMaterial : AvaloniaObject, IMutableExperimentalAcrylicMaterial
{
private Color _effectiveTintColor;
private Color _effectiveLuminosityColor;
static ExperimentalAcrylicMaterial()
{
AffectsRender<ExperimentalAcrylicMaterial>(
TintColorProperty,
BackgroundSourceProperty,
TintOpacityProperty,
MaterialOpacityProperty,
PlatformTransparencyCompensationLevelProperty);
TintColorProperty.Changed.AddClassHandler<ExperimentalAcrylicMaterial>((b, e) =>
{
b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity);
b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor();
});
TintOpacityProperty.Changed.AddClassHandler<ExperimentalAcrylicMaterial>((b, e) =>
{
b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity);
b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor();
});
MaterialOpacityProperty.Changed.AddClassHandler<ExperimentalAcrylicMaterial>((b, e) =>
{
b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity);
b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor();
});
PlatformTransparencyCompensationLevelProperty.Changed.AddClassHandler<ExperimentalAcrylicMaterial>((b, e) =>
{
b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity);
b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor();
});
}
/// <summary>
/// Defines the <see cref="TintColor"/> property.
/// </summary>
public static readonly StyledProperty<Color> TintColorProperty =
AvaloniaProperty.Register<ExperimentalAcrylicMaterial, Color>(nameof(TintColor));
/// <summary>
/// Defines the <see cref="BackgroundSource"/> property.
/// </summary>
public static readonly StyledProperty<AcrylicBackgroundSource> BackgroundSourceProperty =
AvaloniaProperty.Register<ExperimentalAcrylicMaterial, AcrylicBackgroundSource>(nameof(BackgroundSource));
/// <summary>
/// Defines the <see cref="TintOpacity"/> property.
/// </summary>
public static readonly StyledProperty<double> TintOpacityProperty =
AvaloniaProperty.Register<ExperimentalAcrylicMaterial, double>(nameof(TintOpacity), 0.8);
/// <summary>
/// Defines the <see cref="MaterialOpacity"/> property.
/// </summary>
public static readonly StyledProperty<double> MaterialOpacityProperty =
AvaloniaProperty.Register<ExperimentalAcrylicMaterial, double>(nameof(MaterialOpacity), 0.5);
/// <summary>
/// Defines the <see cref="PlatformTransparencyCompensationLevel"/> property.
/// </summary>
public static readonly StyledProperty<double> PlatformTransparencyCompensationLevelProperty =
AvaloniaProperty.Register<ExperimentalAcrylicMaterial, double>(nameof(PlatformTransparencyCompensationLevel), 0.0);
/// <summary>
/// Defines the <see cref="FallbackColor"/> property.
/// </summary>
public static readonly StyledProperty<Color> FallbackColorProperty =
AvaloniaProperty.Register<ExperimentalAcrylicMaterial, Color>(nameof(FallbackColor));
/// <inheritdoc/>
public event EventHandler? Invalidated;
/// <summary>
/// Gets or Sets the BackgroundSource <seealso cref="AcrylicBackgroundSource"/>.
/// </summary>
public AcrylicBackgroundSource BackgroundSource
{
get => GetValue(BackgroundSourceProperty);
set => SetValue(BackgroundSourceProperty, value);
}
/// <summary>
/// Gets or Sets the TintColor.
/// </summary>
public Color TintColor
{
get => GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
/// <summary>
/// Gets or Sets the Tint Opacity.
/// </summary>
public double TintOpacity
{
get => GetValue(TintOpacityProperty);
set => SetValue(TintOpacityProperty, value);
}
/// <summary>
/// Gets or Sets the Fallback Color.
/// This is used on rendering plaforms that dont support acrylic.
/// </summary>
public Color FallbackColor
{
get => GetValue(FallbackColorProperty);
set => SetValue(FallbackColorProperty, value);
}
/// <summary>
/// Gets or Sets the MaterialOpacity.
/// This makes the material more or less opaque.
/// </summary>
public double MaterialOpacity
{
get => GetValue(MaterialOpacityProperty);
set => SetValue(MaterialOpacityProperty, value);
}
/// <summary>
/// Gets or Sets the PlatformTransparencyCompensationLevel.
/// This value defines the minimum <see cref="MaterialOpacity"/> that can be used.
/// It means material opacity is re-scaled from this value to 1.
/// </summary>
public double PlatformTransparencyCompensationLevel
{
get => GetValue(PlatformTransparencyCompensationLevelProperty);
set => SetValue(PlatformTransparencyCompensationLevelProperty, value);
}
Color IExperimentalAcrylicMaterial.MaterialColor => _effectiveLuminosityColor;
Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor;
private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity)
{
// Update tintColor's alpha with the combined opacity value
double tintOpacityModifier = GetTintOpacityModifier(tintColor);
return new Color((byte)(255 * ((255.0 / tintColor.A) * tintOpacity) * tintOpacityModifier), tintColor.R, tintColor.G, tintColor.B);
}
private static double GetTintOpacityModifier(Color tintColor)
{
// This method suppresses 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.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning.
const double whiteMaxOpacity = 0.2; // 100% luminosity
const double midPointMaxOpacity = 0.45; // 50% luminosity
const double blackMaxOpacity = 0.45; // 0% luminosity
var hsv = tintColor.ToHsv();
double opacityModifier = midPointMaxOpacity;
if (hsv.V != midPoint)
{
// Determine maximum suppression amount
double lowestMaxOpacity = midPointMaxOpacity;
double maxDeviation = midPoint;
if (hsv.V > midPoint)
{
lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV)
maxDeviation = 1 - maxDeviation;
}
else if (hsv.V < midPoint)
{
lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV)
}
double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity;
// Determine normalized deviation from the midpoint
double deviation = Math.Abs(hsv.V - midPoint);
double normalizedDeviation = deviation / maxDeviation;
// If we have saturation, reduce opacity suppression to allow that color to come through more
if (hsv.S > 0)
{
// Dampen opacity suppression based on how much saturation there is
maxOpacitySuppression *= Math.Max(1 - (hsv.S * 2), 0.0);
}
double opacitySuppression = maxOpacitySuppression * normalizedDeviation;
opacityModifier = midPointMaxOpacity - opacitySuppression;
}
return opacityModifier;
}
private Color GetEffectiveLuminosityColor()
{
double? luminosityOpacity = MaterialOpacity;
return GetLuminosityColor(luminosityOpacity);
}
private static byte Trim(double value)
{
value = Math.Min(Math.Floor(value * 256), 255);
if (value < 0)
{
return 0;
}
else if (value > 255)
{
return 255;
}
return (byte)value;
}
private static float RGBMax(Color color)
{
if (color.R > color.G)
return (color.R > color.B) ? color.R : color.B;
else
return (color.G > color.B) ? color.G : color.B;
}
private static float RGBMin(Color color)
{
if (color.R < color.G)
return (color.R < color.B) ? color.R : color.B;
else
return (color.G < color.B) ? color.G : color.B;
}
// The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity
private Color GetLuminosityColor(double? luminosityOpacity)
{
// Calculate the HSL lightness value of the color.
var max = (float)RGBMax(TintColor) / 255.0f;
var min = (float)RGBMin(TintColor) / 255.0f;
var lightness = (max + min) / 2.0;
lightness = 1 - ((1 - lightness) * (luminosityOpacity ?? 1));
lightness = 0.13 + (lightness * 0.74);
var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness));
var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel;
return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ((luminosityOpacity ?? 1) * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B);
}
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : ExperimentalAcrylicMaterial
{
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
foreach (var property in properties)
{
property.Changed.Subscribe(invalidateObserver);
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
public IExperimentalAcrylicMaterial ToImmutable()
{
return new ImmutableExperimentalAcrylicMaterial(this);
}
}
}