Browse Source

Merge pull request #4043 from AvaloniaUI/feature/experimental-acrylic-brush

experimental acrylic brush implementation.
pull/4236/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
5a890b9063
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      samples/ControlCatalog/MainView.xaml
  2. 139
      samples/ControlCatalog/Pages/AcrylicPage.xaml
  3. 19
      samples/ControlCatalog/Pages/AcrylicPage.xaml.cs
  4. 23
      src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs
  5. 3
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  6. 117
      src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
  7. 5
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  8. 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  9. 2
      src/Avalonia.Native/WindowImplBase.cs
  10. 20
      src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs
  11. 345
      src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
  12. 2
      src/Avalonia.Visuals/Media/IBrush.cs
  13. 33
      src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs
  14. 14
      src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs
  15. 73
      src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs
  16. 22
      src/Avalonia.Visuals/Media/MaterialExtensions.cs
  17. 9
      src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs
  18. 17
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  19. 94
      src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  20. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  21. 2
      src/Avalonia.X11/X11Window.cs
  22. 2
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  23. BIN
      src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png
  24. 3
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  25. 120
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  26. 2
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  27. 5
      src/Windows/Avalonia.Win32/WindowImpl.cs

5
samples/ControlCatalog/MainView.xaml

@ -2,7 +2,7 @@
xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.MainView"
Background="Transparent"
Background="Black"
Foreground="{DynamicResource ThemeForegroundBrush}"
FontSize="{DynamicResource FontSizeNormal}">
<Grid>
@ -14,6 +14,7 @@
</Style>
</Grid.Styles>
<TabControl Classes="sidebar" Name="Sidebar">
<TabItem Header="Acrylic"><pages:AcrylicPage/></TabItem>
<TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
<TabItem Header="Border"><pages:BorderPage/></TabItem>
<TabItem Header="Button"><pages:ButtonPage/></TabItem>
@ -80,7 +81,7 @@
<ComboBoxItem>Simple - Light</ComboBoxItem>
<ComboBoxItem>Simple - Dark</ComboBoxItem>
</ComboBox>
<ComboBox x:Name="TransparencyLevels" SelectedIndex="0">
<ComboBox x:Name="TransparencyLevels" SelectedIndex="{Binding TransparencyLevel}">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>

139
samples/ControlCatalog/Pages/AcrylicPage.xaml

@ -0,0 +1,139 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.AcrylicPage">
<Border Padding="20" HorizontalAlignment="Center">
<StackPanel Spacing="20">
<ExperimentalAcrylicBorder Width="660" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="White"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
<StackPanel Spacing="5" Margin="40 10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="TintOpacity" Foreground="Black" />
<Slider Name="TintOpacitySlider" Minimum="0" Maximum="1" Value="0.9" Width="400" />
<TextBlock Text="{Binding #TintOpacitySlider.Value}" Foreground="Black" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="MaterialOpacity" Foreground="Black" />
<Slider Name="MaterialOpacitySlider" Minimum="0" Maximum="1" Value="0.8" Width="400" />
<TextBlock Text="{Binding #MaterialOpacitySlider.Value}" Foreground="Black" />
</StackPanel>
</StackPanel>
</ExperimentalAcrylicBorder>
<StackPanel Orientation="Horizontal" Spacing="20">
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#FF0000"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#00FF00"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#000000"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="20">
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#0000FF"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#FFFF00"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#000000"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="20">
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="White"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="#3c3c3c"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder Height="200" Width="200" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="White"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
</StackPanel>
<ExperimentalAcrylicBorder Height="200" Width="660" CornerRadius="5">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
TintColor="Red"
TintOpacity="{Binding #TintOpacitySlider.Value}"
MaterialOpacity="{Binding #MaterialOpacitySlider.Value}"
BackgroundSource="Digger" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
</StackPanel>
</Border>
</UserControl>

19
samples/ControlCatalog/Pages/AcrylicPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class AcrylicPage : UserControl
{
public AcrylicPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

23
src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs

@ -0,0 +1,23 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines compensation levels for the platform depending on the transparency level.
/// It controls the base opacity level of the 'tracing paper' layer that compensates
/// for low blur radius.
/// </summary>
public struct AcrylicPlatformCompensationLevels
{
public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic)
{
TransparentLevel = transparent;
BlurLevel = blurred;
AcrylicBlurLevel = acrylic;
}
public double TransparentLevel { get; }
public double BlurLevel { get; }
public double AcrylicBlurLevel { get; }
}
}

3
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -52,6 +52,9 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;
public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1);

117
src/Avalonia.Controls/ExperimentalAcrylicBorder.cs

@ -0,0 +1,117 @@
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using System;
namespace Avalonia.Controls
{
public class ExperimentalAcrylicBorder : Decorator
{
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ExperimentalAcrylicBorder>();
public static readonly StyledProperty<ExperimentalAcrylicMaterial> MaterialProperty =
AvaloniaProperty.Register<ExperimentalAcrylicBorder, ExperimentalAcrylicMaterial>(nameof(Material));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
private IDisposable _subscription;
static ExperimentalAcrylicBorder()
{
AffectsRender<ExperimentalAcrylicBorder>(
MaterialProperty,
CornerRadiusProperty);
}
/// <summary>
/// Gets or sets the radius of the border rounded corners.
/// </summary>
public CornerRadius CornerRadius
{
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public ExperimentalAcrylicMaterial Material
{
get => GetValue(MaterialProperty);
set => SetValue(MaterialProperty, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var tl = (e.Root as TopLevel);
_subscription = tl.GetObservable(TopLevel.ActualTransparencyLevelProperty)
.Subscribe(x =>
{
switch (x)
{
case WindowTransparencyLevel.Transparent:
case WindowTransparencyLevel.None:
Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.TransparentLevel;
break;
case WindowTransparencyLevel.Blur:
Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.BlurLevel;
break;
case WindowTransparencyLevel.AcrylicBlur:
Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.AcrylicBlurLevel;
break;
}
});
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_subscription?.Dispose();
}
public override void Render(DrawingContext context)
{
if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc)
{
var cornerRadius = CornerRadius;
idc.DrawRectangle(
Material,
new RoundedRect(
new Rect(Bounds.Size),
cornerRadius.TopLeft, cornerRadius.TopRight,
cornerRadius.BottomRight, cornerRadius.BottomLeft));
}
else
{
_borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default);
}
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size availableSize)
{
return LayoutHelper.MeasureChild(Child, availableSize, Padding);
}
/// <summary>
/// Arranges the control's child.
/// </summary>
/// <param name="finalSize">The size allocated to the control.</param>
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
return LayoutHelper.ArrangeChild(Child, finalSize, Padding);
}
}
}

5
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -127,5 +127,10 @@ namespace Avalonia.Platform
/// Gets the current <see cref="WindowTransparencyLevel"/> of the TopLevel.
/// </summary>
WindowTransparencyLevel TransparencyLevel { get; }
/// <summary>
/// Gets the <see cref="AcrylicPlatformCompensationLevels"/> for the platform.
/// </summary>
AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
}
}

2
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -174,6 +174,8 @@ namespace Avalonia.DesignerSupport.Remote
public bool IsClientAreaExtendedToDecorations { get; }
public bool NeedsManagedDecorations => false;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
}
class ClipboardStub : IClipboard

2
src/Avalonia.Native/WindowImplBase.cs

@ -439,6 +439,8 @@ namespace Avalonia.Native
public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0);
public IPlatformHandle Handle { get; private set; }
}
}

20
src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs

@ -0,0 +1,20 @@
namespace Avalonia.Media
{
/// <summary>
/// Background Sources for Acrylic.
/// </summary>
public enum AcrylicBackgroundSource
{
/// <summary>
/// The acrylic has no background.
/// </summary>
None,
/// <summary>
/// Cuts through all render layers to reveal the window background.
/// This means if your window is transparent or blurred it
/// will be blended with the material.
/// </summary>
Digger
}
}

345
src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs

@ -0,0 +1,345 @@
using System;
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 struct HsvColor
{
public float Hue { get; set; }
public float Saturation { get; set; }
public float Value { get; set; }
}
private 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 = v = max;
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 };
}
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 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.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 = RgbToHsv(tintColor);
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;
}
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.Value);
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.Value * 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
{
static void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as T)?.RaiseInvalidated(EventArgs.Empty);
}
foreach (var property in properties)
{
property.Changed.Subscribe(e => Invalidate(e));
}
}
/// <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);
}
}
}

2
src/Avalonia.Visuals/Media/IBrush.cs

@ -13,4 +13,4 @@ namespace Avalonia.Media
/// </summary>
double Opacity { get; }
}
}
}

33
src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs

@ -0,0 +1,33 @@
namespace Avalonia.Media
{
/// <summary>
/// Experimental Interface for producing Acrylic-like materials.
/// </summary>
public interface IExperimentalAcrylicMaterial
{
/// <summary>
/// Gets the <see cref="AcrylicBackgroundSource"/> of the material.
/// </summary>
AcrylicBackgroundSource BackgroundSource { get; }
/// <summary>
/// Gets the TintColor of the material.
/// </summary>
Color TintColor { get; }
/// <summary>
/// Gets the TintOpacity of the material.
/// </summary>
double TintOpacity { get; }
/// <summary>
/// Gets the effective material color.
/// </summary>
Color MaterialColor { get; }
/// <summary>
/// Gets the fallback color.
/// </summary>
Color FallbackColor { get; }
}
}

14
src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs

@ -0,0 +1,14 @@
namespace Avalonia.Media
{
/// <summary>
/// Represents a mutable brush which can return an immutable clone of itself.
/// </summary>
public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender
{
/// <summary>
/// Creates an immutable clone of the brush.
/// </summary>
/// <returns>The immutable clone.</returns>
IExperimentalAcrylicMaterial ToImmutable();
}
}

73
src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs

@ -0,0 +1,73 @@
using System;
namespace Avalonia.Media
{
public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable<ImmutableExperimentalAcrylicMaterial>
{
public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush)
{
BackgroundSource = brush.BackgroundSource;
TintColor = brush.TintColor;
TintOpacity = brush.TintOpacity;
FallbackColor = brush.FallbackColor;
MaterialColor = brush.MaterialColor;
}
public AcrylicBackgroundSource BackgroundSource { get; }
public Color TintColor { get; }
public Color MaterialColor { get; }
public double TintOpacity { get; }
public Color FallbackColor { get; }
public bool Equals(ImmutableExperimentalAcrylicMaterial other)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
return
TintColor == other.TintColor &&
TintOpacity == other.TintOpacity &&
BackgroundSource == other.BackgroundSource &&
FallbackColor == other.FallbackColor && MaterialColor == other.MaterialColor;
}
public override bool Equals(object obj)
{
return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other);
}
public Color GetEffectiveTintColor()
{
return TintColor;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = (hash * 23) + TintColor.GetHashCode();
hash = (hash * 23) + TintOpacity.GetHashCode();
hash = (hash * 23) + BackgroundSource.GetHashCode();
hash = (hash * 23) + FallbackColor.GetHashCode();
hash = (hash * 23) + MaterialColor.GetHashCode();
return hash;
}
}
public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right)
{
return left.Equals(right);
}
public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right)
{
return !left.Equals(right);
}
}
}

22
src/Avalonia.Visuals/Media/MaterialExtensions.cs

@ -0,0 +1,22 @@
using System;
namespace Avalonia.Media
{
public static class MaterialExtensions
{
/// <summary>
/// Converts a brush to an immutable brush.
/// </summary>
/// <param name="material">The brush.</param>
/// <returns>
/// The result of calling <see cref="IMutableBrush.ToImmutable"/> if the brush is mutable,
/// otherwise <paramref name="material"/>.
/// </returns>
public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material)
{
Contract.Requires<ArgumentNullException>(material != null);
return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material;
}
}
}

9
src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs

@ -0,0 +1,9 @@
using Avalonia.Media;
namespace Avalonia.Platform
{
public interface IDrawingContextWithAcrylicLikeSupport
{
void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect);
}
}

17
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A drawing context which builds a scene graph.
/// </summary>
internal class DeferredDrawingContextImpl : IDrawingContextImpl
internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private readonly ISceneBuilder _sceneBuilder;
private VisualNode _node;
@ -164,6 +164,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
/// <inheritdoc/>
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
var next = NextDrawAs<ExperimentalAcrylicNode>();
if (next == null || !next.Item.Equals(Transform, material, rect))
{
Add(new ExperimentalAcrylicNode(Transform, material, rect));
}
else
{
++_drawOperationindex;
}
}
public void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();

94
src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs

@ -0,0 +1,94 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a rectangle draw.
/// </summary>
internal class ExperimentalAcrylicNode : DrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="RectangleNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="boxShadow">The box shadow parameters</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public ExperimentalAcrylicNode(
Matrix transform,
IExperimentalAcrylicMaterial material,
RoundedRect rect)
: base(rect.Rect, transform)
{
Transform = transform;
Material = material?.ToImmutable();
Rect = rect;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
public IExperimentalAcrylicMaterial Material { get; }
/// <summary>
/// Gets the rectangle to draw.
/// </summary>
public RoundedRect Rect { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="rect">The rectangle of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, IExperimentalAcrylicMaterial material, RoundedRect rect)
{
return transform == Transform &&
Equals(material, Material) &&
rect.Equals(Rect);
}
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if(context is IDrawingContextWithAcrylicLikeSupport idc)
{
idc.DrawRectangle(Material, Rect);
}
else
{
context.DrawRectangle(new ImmutableSolidColorBrush(Material.FallbackColor), null, Rect);
}
}
/// <inheritdoc/>
public override bool HitTest(Point p)
{
// TODO: This doesn't respect CornerRadius yet.
if (Transform.HasInverse)
{
p *= Transform.Invert();
if (Material != null)
{
var rect = Rect.Rect;
return rect.Contains(p);
}
}
return false;
}
}
}

2
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="boxShadow">The box shadow parameters</param>
/// <param name="boxShadows">The box shadow parameters</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public RectangleNode(
Matrix transform,

2
src/Avalonia.X11/X11Window.cs

@ -1128,6 +1128,8 @@ namespace Avalonia.X11
public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);
public bool NeedsManagedDecorations => false;
}
}

2
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@ -82,5 +82,7 @@ namespace Avalonia.LinuxFramebuffer
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
}
}

BIN
src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

3
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -7,6 +7,9 @@
<IncludeLinuxSkia>true</IncludeLinuxSkia>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\NoiseAsset_256X256_PNG.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>

120
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -17,7 +17,7 @@ namespace Avalonia.Skia
/// <summary>
/// Skia based drawing context.
/// </summary>
internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl
internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private IDisposable[] _disposables;
private readonly Vector _dpi;
@ -34,6 +34,7 @@ namespace Avalonia.Skia
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
private readonly SKPaint _boxShadowPaint = new SKPaint();
private static SKShader s_acrylicNoiseShader;
/// <summary>
/// Context create info.
@ -227,6 +228,41 @@ namespace Avalonia.Skia
return bounds;
}
/// <inheritdoc />
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
return;
var rc = rect.Rect.ToSKRect();
var isRounded = rect.IsRounded;
var needRoundRect = rect.IsRounded;
using var skRoundRect = needRoundRect ? new SKRoundRect() : null;
if (needRoundRect)
skRoundRect.SetRectRadii(rc,
new[]
{
rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(),
rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(),
});
if (material != null)
{
using (var paint = CreateAcrylicPaint(_fillPaint, material))
{
if (isRounded)
{
Canvas.DrawRoundRect(skRoundRect, paint.Paint);
}
else
{
Canvas.DrawRect(rc, paint.Paint);
}
}
}
}
/// <inheritdoc />
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default)
@ -659,6 +695,86 @@ namespace Avalonia.Skia
}
}
static SKColorFilter CreateAlphaColorFilter(double opacity)
{
if (opacity > 1)
opacity = 1;
var c = new byte[256];
var a = new byte[256];
for (var i = 0; i < 256; i++)
{
c[i] = (byte)i;
a[i] = (byte)(i * opacity);
}
return SKColorFilter.CreateTable(a, c, c, c);
}
static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha)
{
var ca = leftColor / 255d;
var aa = leftAlpha / 255d;
var cb = rightColor / 255d;
var ab = rightAlpha / 255d;
var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa));
return (byte)(r * 255);
}
static Color Blend(Color left, Color right)
{
var aa = left.A / 255d;
var ab = right.A / 255d;
return new Color(
(byte)((aa + ab * (1 - aa)) * 255),
Blend(left.R, left.A, right.R, right.A),
Blend(left.G, left.A, right.G, right.A),
Blend(left.B, left.A, right.B, right.A)
);
}
internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, bool disposePaint = false)
{
var paintWrapper = new PaintWrapper(paint, disposePaint);
paint.IsAntialias = true;
double opacity = _currentOpacity;
var tintOpacity =
material.BackgroundSource == AcrylicBackgroundSource.Digger ?
material.TintOpacity : 1;
const double noiseOpcity = 0.0225;
var tintColor = material.TintColor;
var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A);
if (s_acrylicNoiseShader == null)
{
using (var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png"))
using (var bitmap = SKBitmap.Decode(stream))
{
s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat)
.WithColorFilter(CreateAlphaColorFilter(noiseOpcity));
}
}
using (var backdrop = SKShader.CreateColor(new SKColor(material.MaterialColor.R, material.MaterialColor.G, material.MaterialColor.B, material.MaterialColor.A)))
using (var tintShader = SKShader.CreateColor(tint))
using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader))
using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader))
{
paint.Shader = compose;
if (material.BackgroundSource == AcrylicBackgroundSource.Digger)
{
paint.BlendMode = SKBlendMode.Src;
}
return paintWrapper;
}
}
/// <summary>
/// Creates paint wrapper for given brush.
/// </summary>
@ -811,7 +927,7 @@ namespace Avalonia.Skia
};
return new SurfaceRenderTarget(createInfo);
}
}
/// <summary>
/// Skia cached paint state.

2
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@ -256,5 +256,7 @@ namespace Avalonia.Win32.Interop.Wpf
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
}
}

5
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -277,7 +277,7 @@ namespace Avalonia.Win32
}
accent.AccentFlags = 2;
accent.GradientColor = 0x00FFFFFF;
accent.GradientColor = 0x01000000;
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
Marshal.StructureToPtr(accent, accentPtr, false);
@ -1071,6 +1071,9 @@ namespace Avalonia.Win32
/// <inheritdoc/>
public Thickness OffScreenMargin => _offScreenMargin;
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0);
private struct SavedWindowInfo
{
public WindowStyles Style { get; set; }

Loading…
Cancel
Save