Browse Source

Bitmap effects support

pull/11014/head
Nikita Tsukanov 3 years ago
parent
commit
cf28998a46
  1. 40
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 22
      src/Avalonia.Base/Media/Effects/BlurEffect.cs
  3. 104
      src/Avalonia.Base/Media/Effects/DropShadowEffect.cs
  4. 93
      src/Avalonia.Base/Media/Effects/Effect.cs
  5. 131
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  6. 18
      src/Avalonia.Base/Media/Effects/EffectConverter.cs
  7. 56
      src/Avalonia.Base/Media/Effects/EffectExtesions.cs
  8. 83
      src/Avalonia.Base/Media/Effects/EffectTransition.cs
  9. 29
      src/Avalonia.Base/Media/Effects/IBlurEffect.cs
  10. 84
      src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs
  11. 26
      src/Avalonia.Base/Media/Effects/IEffect.cs
  12. 6
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  13. 9
      src/Avalonia.Base/Rect.cs
  14. 10
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  15. 56
      src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
  16. 15
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  17. 68
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  18. 28
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  19. 20
      src/Avalonia.Base/Visual.cs
  20. 6
      src/Avalonia.Base/composition-schema.xml
  21. 50
      src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs
  22. 16
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  23. 73
      tests/Avalonia.Base.UnitTests/Media/EffectTests.cs
  24. 43
      tests/Avalonia.RenderTests/Media/EffectTests.cs
  25. 3
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  26. BIN
      tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png

40
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -308,6 +308,41 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Blur">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="blur(0)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="blur(10)"/>
</KeyFrame>
</Animation>
</Style.Animations>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
</Style>
<Style Selector="Border.DropShadow">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="drop-shadow(0 0 0)"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="Effect" Value="drop-shadow(5 5 0 Green)"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="Effect" Value="drop-shadow(5 5 5 Red)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="drop-shadow(20 -5 5 Blue)"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -332,6 +367,11 @@
<Border Classes="Test Rect8" Child="{x:Null}" />
<Border Classes="Test Rect9" Child="{x:Null}" />
<Border Classes="Test Rect10" Child="{x:Null}" />
<Border Classes="Test Blur" Background="#ffa0a0a0" BorderThickness="4" BorderBrush="Yellow" Padding="10"/>
<Border Classes="Test DropShadow" Background="Transparent" BorderThickness="4" BorderBrush="Yellow">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">Drop
Shadow</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</Grid>

22
src/Avalonia.Base/Media/Effects/BlurEffect.cs

@ -0,0 +1,22 @@
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public class BlurEffect : Effect, IBlurEffect, IMutableEffect
{
public static readonly StyledProperty<double> RadiusProperty = AvaloniaProperty.Register<BlurEffect, double>(
nameof(Radius), 5);
public double Radius
{
get => GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
static BlurEffect()
{
AffectsRender<BlurEffect>(RadiusProperty);
}
public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius);
}

104
src/Avalonia.Base/Media/Effects/DropShadowEffect.cs

@ -0,0 +1,104 @@
// ReSharper disable once CheckNamespace
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public abstract class DropShadowEffectBase : Effect
{
public static readonly StyledProperty<double> BlurRadiusProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(BlurRadius), 5);
public double BlurRadius
{
get => GetValue(BlurRadiusProperty);
set => SetValue(BlurRadiusProperty, value);
}
public static readonly StyledProperty<Color> ColorProperty = AvaloniaProperty.Register<DropShadowEffectBase, Color>(
nameof(Color));
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(Opacity), 1);
public double Opacity
{
get => GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
static DropShadowEffectBase()
{
AffectsRender<DropShadowEffectBase>(BlurRadiusProperty, ColorProperty, OpacityProperty);
}
}
public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> OffsetXProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetX), 3.5355);
public double OffsetX
{
get => GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
public static readonly StyledProperty<double> OffsetYProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetY), 3.5355);
public double OffsetY
{
get => GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
static DropShadowEffect()
{
AffectsRender<DropShadowEffect>(OffsetXProperty, OffsetYProperty);
}
public IImmutableEffect ToImmutable()
{
return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}
}
/// <summary>
/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY
/// </summary>
public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> ShadowDepthProperty =
AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(ShadowDepth), 5);
public double ShadowDepth
{
get => GetValue(ShadowDepthProperty);
set => SetValue(ShadowDepthProperty, value);
}
public static readonly StyledProperty<double> DirectionProperty = AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(Direction), 315);
public double Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public double OffsetX => Math.Cos(Direction) * ShadowDepth;
public double OffsetY => Math.Sin(Direction) * ShadowDepth;
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}

93
src/Avalonia.Base/Media/Effects/Effect.cs

@ -0,0 +1,93 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public class Effect : Animatable, IAffectsRender
{
/// <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 : Effect
{
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);
/// <inheritdoc />
public event EventHandler? Invalidated;
static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s);
public static IEffect Parse(string s)
{
var span = s.AsSpan();
var r = new TokenParser(span);
if (r.TryConsume("blur"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableBlurEffect(radius);
}
if (r.TryConsume("drop-shadow"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX)
|| !r.TryParseDouble(out var offsetY))
throw ParseError(s);
double blurRadius = 0;
var color = Colors.Black;
if (!r.TryConsume(')'))
{
if (!r.TryParseDouble(out blurRadius) || blurRadius < 0)
throw ParseError(s);
if (!r.TryConsume(')'))
{
var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal);
if (endOfExpression == -1)
throw ParseError(s);
if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace())
throw ParseError(s);
if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color))
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
}
if (!r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
throw ParseError(s);
}
static Effect()
{
EffectAnimator.EnsureRegistered();
}
}

131
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -0,0 +1,131 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation.Animators;
public class EffectAnimator : Animator<IEffect?>
{
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> match, Action? onComplete)
{
if (TryCreateAnimator<BlurEffectAnimator, IBlurEffect>(out var animator)
|| TryCreateAnimator<DropShadowEffectAnimator, IDropShadowEffect>(out animator))
return animator.Apply(animation, control, clock, match, onComplete);
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
return base.Apply(animation, control, clock, match, onComplete);
}
private bool TryCreateAnimator<TAnimator, TInterface>([NotNullWhen(true)] out IAnimator? animator)
where TAnimator : EffectAnimatorBase<TInterface>, new() where TInterface : class, IEffect
{
TAnimator? createdAnimator = null;
foreach (var keyFrame in this)
{
if (keyFrame.Value is TInterface)
{
createdAnimator ??= new TAnimator()
{
Property = Property
};
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
Value = keyFrame.Value
});
}
else
{
animator = null;
return false;
}
}
animator = createdAnimator;
return animator != null;
}
/// <summary>
/// Fallback implementation of <see cref="IEffect"/> animation.
/// </summary>
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue;
private static bool s_Registered;
public static void EnsureRegistered()
{
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}
public abstract class EffectAnimatorBase<T> : Animator<IEffect?> where T : class, IEffect?
{
public override IDisposable BindAnimation(Animatable control, IObservable<IEffect?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IEffect?>)Property, instance, BindingPriority.Animation);
}
protected abstract T Interpolate(double progress, T oldValue, T newValue);
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue)
{
var old = oldValue as T;
var n = newValue as T;
if (old == null || n == null)
return progress >= 0.5 ? newValue : oldValue;
return Interpolate(progress, old, n);
}
}
public class BlurEffectAnimator : EffectAnimatorBase<IBlurEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue)
{
return new ImmutableBlurEffect(
s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius));
}
}
public class DropShadowEffectAnimator : EffectAnimatorBase<IDropShadowEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue,
IDropShadowEffect newValue)
{
var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius);
var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color);
var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity);
if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection)
{
return new ImmutableDropShadowDirectionEffect(
s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction),
s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth),
blur, color, opacity
);
}
return new ImmutableDropShadowEffect(
s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
blur, color, opacity
);
}
}

18
src/Avalonia.Base/Media/Effects/EffectConverter.cs

@ -0,0 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Media;
public class EffectConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
{
return value is string s ? Effect.Parse(s) : null;
}
}

56
src/Avalonia.Base/Media/Effects/EffectExtesions.cs

@ -0,0 +1,56 @@
using System;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public static class EffectExtensions
{
static double AdjustPaddingRadius(double radius)
{
if (radius <= 0)
return 0;
return Math.Ceiling(radius) + 1;
}
internal static Thickness GetEffectOutputPadding(this IEffect? effect)
{
if (effect == null)
return default;
if (effect is IBlurEffect blur)
return new Thickness(AdjustPaddingRadius(blur.Radius));
if (effect is IDropShadowEffect dropShadowEffect)
{
var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius);
var rc = new Rect(-radius, -radius,
radius * 2, radius * 2);
rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY));
return new Thickness(Math.Max(0, 0 - rc.X),
Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom));
}
throw new ArgumentException("Unknown effect type: " + effect.GetType());
}
/// <summary>
/// Converts a effect to an immutable effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <returns>
/// The result of calling <see cref="IMutableEffect.ToImmutable"/> if the effect is mutable,
/// otherwise <paramref name="effect"/>.
/// </returns>
public static IImmutableEffect ToImmutable(this IEffect effect)
{
_ = effect ?? throw new ArgumentNullException(nameof(effect));
return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect;
}
internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right)
{
if (immutable == null && right == null)
return true;
if (immutable != null && right != null)
return immutable.Equals(right);
return false;
}
}

83
src/Avalonia.Base/Media/Effects/EffectTransition.cs

@ -0,0 +1,83 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation;
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IEffect"/> type.
/// </summary>
public class EffectTransition : Transition<IEffect?>
{
private static readonly BlurEffectAnimator s_blurEffectAnimator = new();
private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new();
private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0);
private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0);
bool TryWithAnimator<TAnimator, TInterface>(
IObservable<double> progress,
TAnimator animator,
IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable<IEffect?> observable)
where TAnimator : EffectAnimatorBase<TInterface> where TInterface : class, IEffect
{
observable = null;
TInterface? oldI = null, newI = null;
if (oldValue is TInterface oi)
{
oldI = oi;
if (newValue is TInterface ni)
newI = ni;
else if (newValue == null)
newI = defaultValue;
else
return false;
}
else if (newValue is TInterface nv)
{
oldI = defaultValue;
newI = nv;
}
else
return false;
observable = new AnimatorTransitionObservable<IEffect?, Animator<IEffect?>>(animator, progress, Easing, oldI, newI);
return true;
}
public override IObservable<IEffect?> DoTransition(IObservable<double> progress, IEffect? oldValue, IEffect? newValue)
{
if ((oldValue != null || newValue != null)
&& (
TryWithAnimator<BlurEffectAnimator, IBlurEffect>(progress, s_blurEffectAnimator,
oldValue, newValue, s_DefaultBlur, out var observable)
|| TryWithAnimator<DropShadowEffectAnimator, IDropShadowEffect>(progress, s_dropShadowEffectAnimator,
oldValue, newValue, s_DefaultDropShadow, out observable)
))
return observable;
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IEffect?>
{
private readonly IEffect? _from;
private readonly IEffect? _to;
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing)
{
_from = from;
_to = to;
}
protected override IEffect? ProduceValue(double progress)
{
return progress >= 0.5 ? _to : _from;
}
}
}

29
src/Avalonia.Base/Media/Effects/IBlurEffect.cs

@ -0,0 +1,29 @@
// ReSharper disable once CheckNamespace
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IBlurEffect : IEffect
{
double Radius { get; }
}
public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect
{
static ImmutableBlurEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableBlurEffect(double radius)
{
Radius = radius;
}
public double Radius { get; }
public bool Equals(IEffect? other) =>
// ReSharper disable once CompareOfFloatsByEqualityOperator
other is IBlurEffect blur && blur.Radius == Radius;
}

84
src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs

@ -0,0 +1,84 @@
// ReSharper disable once CheckNamespace
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IDropShadowEffect : IEffect
{
double OffsetX { get; }
double OffsetY { get; }
double BlurRadius { get; }
Color Color { get; }
double Opacity { get; }
}
internal interface IDirectionDropShadowEffect : IDropShadowEffect
{
double Direction { get; }
double ShadowDepth { get; }
}
public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity)
{
OffsetX = offsetX;
OffsetY = offsetY;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX { get; }
public double OffsetY { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}
public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowDirectionEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity)
{
Direction = direction;
ShadowDepth = shadowDepth;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX => Math.Cos(Direction) * ShadowDepth;
public double OffsetY => Math.Sin(Direction) * ShadowDepth;
public double Direction { get; }
public double ShadowDepth { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}

26
src/Avalonia.Base/Media/Effects/IEffect.cs

@ -0,0 +1,26 @@
// ReSharper disable once CheckNamespace
using System;
using System.ComponentModel;
namespace Avalonia.Media;
[TypeConverter(typeof(EffectConverter))]
public interface IEffect
{
}
public interface IMutableEffect : IEffect, IAffectsRender
{
/// <summary>
/// Creates an immutable clone of the effect.
/// </summary>
/// <returns>The immutable clone.</returns>
internal IImmutableEffect ToImmutable();
}
public interface IImmutableEffect : IEffect, IEquatable<IEffect>
{
}

6
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -180,6 +180,12 @@ namespace Avalonia.Platform
object? GetFeature(Type t);
}
public interface IDrawingContextImplWithEffects
{
void PushEffect(IEffect effect);
void PopEffect();
}
public static class DrawingContextImplExtensions
{
/// <summary>

9
src/Avalonia.Base/Rect.cs

@ -526,6 +526,15 @@ namespace Avalonia
}
}
internal static Rect? Union(Rect? left, Rect? right)
{
if (left == null)
return right;
if (right == null)
return left;
return left.Value.Union(right.Value);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified X position.
/// </summary>

10
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -252,8 +252,14 @@ public class CompositingRenderer : IRendererWithCompositor
comp.Opacity = (float)visual.Opacity;
comp.ClipToBounds = visual.ClipToBounds;
comp.Clip = visual.Clip?.PlatformImpl;
comp.OpacityMask = visual.OpacityMask;
if (!Equals(comp.OpacityMask, visual.OpacityMask))
comp.OpacityMask = visual.OpacityMask?.ToImmutable();
if (!comp.Effect.EffectEquals(visual.Effect))
comp.Effect = visual.Effect?.ToImmutable();
var renderTransform = Matrix.Identity;
if (visual.HasMirrorTransform)

56
src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs

@ -29,6 +29,8 @@ namespace Avalonia.Rendering.Composition.Expressions
}
}
public bool NextIsWhitespace() => _s.Length > 0 && char.IsWhiteSpace(_s[0]);
static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z');
@ -238,6 +240,12 @@ namespace Avalonia.Rendering.Composition.Expressions
len = c + 1;
dotCount++;
}
else if (ch == '-')
{
if (len != 0)
return false;
len = c + 1;
}
else
break;
}
@ -254,7 +262,55 @@ namespace Avalonia.Rendering.Composition.Expressions
Advance(len);
return true;
}
public bool TryParseDouble(out double res)
{
res = 0;
SkipWhitespace();
if (_s.Length == 0)
return false;
var len = 0;
var dotCount = 0;
for (var c = 0; c < _s.Length; c++)
{
var ch = _s[c];
if (ch >= '0' && ch <= '9')
len = c + 1;
else if (ch == '.' && dotCount == 0)
{
len = c + 1;
dotCount++;
}
else if (ch == '-')
{
if (len != 0)
return false;
len = c + 1;
}
else
break;
}
var span = _s.Slice(0, len);
#if NETSTANDARD2_0
if (!double.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#else
if (!double.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#endif
Advance(len);
return true;
}
public bool IsEofWithWhitespace()
{
SkipWhitespace();
return Length == 0;
}
public override string ToString() => _s.ToString();
}

15
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -18,7 +18,8 @@ namespace Avalonia.Rendering.Composition.Server;
/// they have information about the full render transform (they are not)
/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation.
/// </summary>
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal class CompositorDrawingContextProxy : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects
{
private IDrawingContextImpl _impl;
@ -155,4 +156,16 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic)
acrylic.DrawRectangle(material, rect);
}
public void PushEffect(IEffect effect)
{
if (_impl is IDrawingContextImplWithEffects effects)
effects.PushEffect(effect);
}
public void PopEffect()
{
if (_impl is IDrawingContextImplWithEffects effects)
effects.PopEffect();
}
}

68
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -1,4 +1,6 @@
using System;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server
internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
{
public ServerCompositionVisualCollection Children { get; private set; } = null!;
private Rect? _transformedContentBounds;
private IImmutableEffect? _oldEffect;
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
@ -24,18 +28,76 @@ namespace Avalonia.Rendering.Composition.Server
}
}
public override void Update(ServerCompositionTarget root)
public override UpdateResult Update(ServerCompositionTarget root)
{
base.Update(root);
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
child.Update(root);
{
var res = child.Update(root);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = Rect.Union(combinedBounds, res.Bounds);
}
}
// If effect is changed, we need to clean both old and new bounds
var effectChanged = !Effect.EffectEquals(_oldEffect);
if (effectChanged)
oldInvalidated = newInvalidated = true;
// Expand invalidated bounds to the whole content area since we don't actually know what is being sampled
// We also ignore clip for now since we don't have means to reset it?
if (_oldEffect != null && oldInvalidated && _transformedContentBounds.HasValue)
AddEffectPaddedDirtyRect(_oldEffect, _transformedContentBounds.Value);
if (Effect != null && newInvalidated && combinedBounds.HasValue)
AddEffectPaddedDirtyRect(Effect, combinedBounds.Value);
_oldEffect = Effect;
_transformedContentBounds = combinedBounds;
IsDirtyComposition = false;
return new(_transformedContentBounds, oldInvalidated, newInvalidated);
}
void AddEffectPaddedDirtyRect(IImmutableEffect effect, Rect transformedBounds)
{
var padding = effect.GetEffectOutputPadding();
if (padding == default)
{
AddDirtyRect(transformedBounds);
return;
}
// We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones
// Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare
// we instead apply the transformation matrix to rescale the bounds
// If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix is
{
M12: 0, M13: 0, M14: 0,
M21: 0, M23: 0, M24: 0,
M31: 0, M32: 0, M34: 0,
M43: 0, M44: 1
})
padding = new Thickness(padding.Left * CombinedTransformMatrix.M11,
padding.Top * CombinedTransformMatrix.M22,
padding.Right * CombinedTransformMatrix.M11,
padding.Bottom * CombinedTransformMatrix.M22);
else
{
// Conservatively use the transformed rect size
var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix);
padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height));
}
AddDirtyRect(transformedBounds.Inflate(padding));
}
partial void Initialize()

28
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -54,6 +54,9 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (Effect != null)
canvas.PushEffect(Effect);
if (Opacity != 1)
canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
@ -79,6 +82,9 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopClip();
if (Opacity != 1)
canvas.PopOpacity();
if (Effect != null)
canvas.PopEffect();
}
protected virtual bool HandlesClipToBounds => false;
@ -101,10 +107,18 @@ namespace Avalonia.Rendering.Composition.Server
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; }
public virtual void Update(ServerCompositionTarget root)
public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
{
public UpdateResult() : this(null, false, false)
{
}
}
public virtual UpdateResult Update(ServerCompositionTarget root)
{
if (Parent == null && Root == null)
return;
return default;
var wasVisible = IsVisibleInFrame;
@ -146,6 +160,11 @@ namespace Avalonia.Rendering.Composition.Server
GlobalTransformMatrix = newTransform;
var ownBounds = OwnContentBounds;
// Since padding is applied in the current visual's coordinate space we expand bounds before transforming them
if (Effect != null)
ownBounds = ownBounds.Inflate(Effect.GetEffectOutputPadding());
if (ownBounds != _oldOwnContentBounds || positionChanged)
{
_oldOwnContentBounds = ownBounds;
@ -168,7 +187,7 @@ namespace Avalonia.Rendering.Composition.Server
_combinedTransformedClipBounds =
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
@ -208,9 +227,10 @@ namespace Avalonia.Rendering.Composition.Server
readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsHitTestVisibleInFrame;
return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds);
}
void AddDirtyRect(Rect rc)
protected void AddDirtyRect(Rect rc)
{
if (rc == default)
return;

20
src/Avalonia.Base/Visual.cs

@ -48,7 +48,7 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<Geometry?> ClipProperty =
AvaloniaProperty.Register<Visual, Geometry?>(nameof(Clip));
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
@ -66,6 +66,12 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
AvaloniaProperty.Register<Visual, IBrush?>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="Effect"/> property.
/// </summary>
public static readonly StyledProperty<IEffect?> EffectProperty =
AvaloniaProperty.Register<Visual, IEffect?>(nameof(Effect));
/// <summary>
/// Defines the <see cref="HasMirrorTransform"/> property.
@ -127,6 +133,8 @@ namespace Avalonia
ClipToBoundsProperty,
IsVisibleProperty,
OpacityProperty,
OpacityMaskProperty,
EffectProperty,
HasMirrorTransformProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
@ -233,6 +241,16 @@ namespace Avalonia
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
}
/// <summary>
/// Gets or sets the effect of the control.
/// </summary>
public IEffect? Effect
{
get => GetValue(EffectProperty);
set => SetValue(EffectProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether to apply mirror transform on this control.

6
src/Avalonia.Base/composition-schema.xml

@ -6,7 +6,8 @@
<Using>Avalonia.Rendering.Composition.Animations</Using>
<Manual Name="Avalonia.Platform.IGeometryImpl" Passthrough="true"/>
<Manual Name="Avalonia.Media.IBrush" Passthrough="true"/>
<Manual Name="Avalonia.Media.IImmutableBrush" Passthrough="true"/>
<Manual Name="Avalonia.Media.IImmutableEffect" Passthrough="true"/>
<Manual Name="CompositionSurface" />
<Manual Name="CompositionDrawingSurface" />
<Object Name="CompositionVisual" Abstract="true">
@ -27,7 +28,8 @@
<Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IImmutableBrush?" Internal="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">

50
src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs

@ -0,0 +1,50 @@
using System;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia;
partial class DrawingContextImpl
{
public void PushEffect(IEffect effect)
{
CheckLease();
using var filter = CreateEffect(effect);
var paint = SKPaintCache.Shared.Get();
paint.ImageFilter = filter;
Canvas.SaveLayer(paint);
SKPaintCache.Shared.ReturnReset(paint);
}
public void PopEffect()
{
CheckLease();
Canvas.Restore();
}
SKImageFilter? CreateEffect(IEffect effect)
{
if (effect is IBlurEffect blur)
{
if (blur.Radius <= 0)
return null;
var sigma = SkBlurRadiusToSigma(blur.Radius);
return SKImageFilter.CreateBlur(sigma, sigma);
}
if (effect is IDropShadowEffect drop)
{
var sigma = drop.BlurRadius > 0 ? SkBlurRadiusToSigma(drop.BlurRadius) : 0;
var alpha = drop.Color.A * drop.Opacity;
if (!_useOpacitySaveLayer)
alpha *= _currentOpacity;
var color = new SKColor(drop.Color.R, drop.Color.G, drop.Color.B, (byte)Math.Max(0, Math.Min(255, alpha)));
return SKImageFilter.CreateDropShadow((float)drop.OffsetX, (float)drop.OffsetY, sigma, sigma, color);
}
return null;
}
}

16
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -19,7 +19,9 @@ namespace Avalonia.Skia
/// <summary>
/// Skia based drawing context.
/// </summary>
internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal partial class DrawingContextImpl : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport,
IDrawingContextImplWithEffects
{
private IDisposable?[]? _disposables;
private readonly Vector _dpi;
@ -249,6 +251,12 @@ namespace Avalonia.Skia
}
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
private struct BoxShadowFilter : IDisposable
{
public readonly SKPaint Paint;
@ -262,12 +270,6 @@ namespace Avalonia.Skia
ClipOperation = clipOperation;
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity)
{
var ac = shadow.Color;

73
tests/Avalonia.Base.UnitTests/Media/EffectTests.cs

@ -0,0 +1,73 @@
using System;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Base.UnitTests.Media;
public class EffectTests
{
[Fact]
public void Parse_Parses_Blur()
{
var effect = (ImmutableBlurEffect)Effect.Parse("blur(123.34)");
Assert.Equal(123.34, effect.Radius);
}
private const uint Black = 0xff000000;
[Theory,
InlineData("drop-shadow(10 20)", 10, 20, 0, Black),
InlineData("drop-shadow( 10 20 ) ", 10, 20, 0, Black),
InlineData("drop-shadow( 10 20 30 ) ", 10, 20, 30, Black),
InlineData("drop-shadow(10 20 30)", 10, 20, 30, Black),
InlineData("drop-shadow(-10 -20 30)", -10, -20, 30, Black),
InlineData("drop-shadow(10 20 30 #ffff00ff)", 10, 20, 30, 0xffff00ff),
InlineData("drop-shadow ( 10 20 30 #ffff00ff ) ", 10, 20, 30, 0xffff00ff),
InlineData("drop-shadow(10 20 30 red)", 10, 20, 30, 0xffff0000),
InlineData("drop-shadow ( 10 20 30 red ) ", 10, 20, 30, 0xffff0000),
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%))", 10, 20, 30, 0x90641e2d),
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%) ) ", 10, 20, 30, 0x90641e2d),
]
public void Parse_Parses_DropShadow(string s, double x, double y, double r, uint color)
{
var effect = (ImmutableDropShadowEffect)Effect.Parse(s);
Assert.Equal(x, effect.OffsetX);
Assert.Equal(y, effect.OffsetY);
Assert.Equal(r, effect.BlurRadius);
Assert.Equal(1, effect.Opacity);
}
[Theory,
InlineData("blur"),
InlineData("blur("),
InlineData("blur()"),
InlineData("blur(123"),
InlineData("blur(aaab)"),
InlineData("drop-shadow(-10 -20 -30)"),
]
public void Invalid_Effect_Parse_Fails(string b)
{
Assert.Throws<ArgumentException>(() => Effect.Parse(b));
}
[Theory,
InlineData("blur(2.5)", 4, 4, 4, 4),
InlineData("blur(0)", 0, 0, 0, 0),
InlineData("drop-shadow(10 15)", 0, 0, 10, 15),
InlineData("drop-shadow(10 15 5)", 0, 0, 16, 21),
InlineData("drop-shadow(0 0 5)", 6, 6, 6, 6),
InlineData("drop-shadow(3 3 5)", 3, 3, 9, 9)
]
public static void PaddingIsCorrectlyCalculated(string effect, double left, double top, double right, double bottom)
{
var padding = Effect.Parse(effect).GetEffectOutputPadding();
Assert.Equal(left, padding.Left);
Assert.Equal(top, padding.Top);
Assert.Equal(right, padding.Right);
Assert.Equal(bottom, padding.Bottom);
}
}

43
tests/Avalonia.RenderTests/Media/EffectTests.cs

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
#pragma warning disable CS0649
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
public class EffectTests : TestBase
{
public EffectTests() : base(@"Media\Effects")
{
}
[Fact]
public async Task DropShadowEffect()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Border()
{
Background = null,
Margin = new Thickness(40),
Effect = new ImmutableDropShadowEffect(20, 30, 5, Colors.Green, 1),
Child = new Border
{
Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 255)),
BorderBrush = Brushes.Red,
BorderThickness = new Thickness(5)
}
}
};
await RenderToFile(target);
CompareImages(skipImmediate: true);
}
}
#endif

3
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -6,6 +6,9 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />
<Compile Update="..\Avalonia.RenderTests\Media\EffectTests.cs">
<Link>Media\EffectTests.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />

BIN
tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Loading…
Cancel
Save