csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
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
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);
|
|
}
|
|
}
|
|
}
|
|
|