Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Composition Shapes/Paths, Easing Functions, Geometry Animations, and more #18267

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f663555
feat(composition): Initial work on Composition Paths
ahmed605 Jan 27, 2024
0b3d8ee
feat(composition): Partially implement CompositionPathGeometry
ahmed605 Jan 30, 2024
0c16861
feat(composition): Implemented CanvasPathBuilder + few fixes for Comp…
ahmed605 Jan 31, 2024
0a5cee0
feat(composition): Partially implemented CanvasGeometry + UI samples
ahmed605 Jan 31, 2024
62ee2b1
feat(composition): Implement more CanvasGeometry methods + more tests
ahmed605 Feb 3, 2024
c029f28
feat(composition): More work on CanvasGeometry, CanvasPathBuilder, an…
ahmed605 Feb 5, 2024
ae22c16
fix(composition): Fix RedirectVisual
ahmed605 Jul 25, 2024
a714c87
fix(composition): remove unnecessary canvas save
ahmed605 Jul 25, 2024
c49dac0
feat(composition): High DPI support for ISkiaSurface
ahmed605 Jul 27, 2024
3b47eed
feat(composition): add Path Shape UI test for CompositionSpriteShape
ahmed605 Aug 4, 2024
93ee9d7
feat(composition): add more CompositionSpriteShape tests
ahmed605 Aug 5, 2024
861ec9b
feat(composition): add support for all geometry types for Composition…
ahmed605 Aug 8, 2024
2d436c7
fix(composition): Fix a crash that can happen when setting Compositio…
ahmed605 Aug 9, 2024
b6bf6fc
feat(composition): add Composition Path Clipping sample
ahmed605 Aug 9, 2024
30b0407
feat(composition): animations support for CompositionGeometry + sample
ahmed605 Aug 13, 2024
d385450
fix(composition): fix keyframe animation locking on animating sub-pro…
ahmed605 Aug 13, 2024
7c12cc4
feat(composition): initial implementation of AnimationController
ahmed605 Aug 14, 2024
d50c934
feat(composition): implement property animator for AnimationController
ahmed605 Aug 14, 2024
4625b97
feat(composition): more work on AnimationController
ahmed605 Aug 14, 2024
1a094f9
feat(composition): initial support for Composition easing functions
ahmed605 Sep 9, 2024
8e9c964
feat(composition): implement all Composition easing functions
ahmed605 Sep 21, 2024
01834b1
chore(composition): fix build
ahmed605 Sep 21, 2024
4616ed2
feat(composition): add easing functions UI tests
ahmed605 Sep 21, 2024
0409f7e
chore(composition): expose Composition internals to Windows projects
ahmed605 Sep 21, 2024
266cfa4
fix(composition): fix CompositionSpriteShape tests
ahmed605 Sep 30, 2024
58d03d7
chore(composition): fix build on WinUI
ahmed605 Sep 30, 2024
f89b253
chore(composition): fix composition samples WinUI build
ahmed605 Sep 30, 2024
32ccaa2
chore(composition): make SkiaGeometrySource2D internal
ahmed605 Oct 1, 2024
b4e6262
Merge branch 'master' into ahmed605/comp-paths
ahmed605 Oct 12, 2024
5b8b132
chore(composition): fix WinUI build
ahmed605 Oct 12, 2024
1eb493b
Merge branch 'master' into ahmed605/comp-paths
ahmed605 Nov 25, 2024
488b599
feat(composition): multi-window High DPI support for ISkiaSurface
ahmed605 Nov 25, 2024
ec1426a
Merge branch 'ahmed605/comp-paths' of https://github.com/unoplatform/…
ahmed605 Nov 25, 2024
92671b2
chore(composition): code style fix
ahmed605 Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/SamplesApp/UITests.Shared/UITests.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\CompositionPathTests.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Core\CloseRequestedTests.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down Expand Up @@ -6095,6 +6099,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\RotatingCubeGlCanvasElement.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\SimpleTriangleGlCanvasElement.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\SKCanvasElementImpl.skia.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\CompositionPathTests.xaml.cs">
<DependentUpon>CompositionPathTests.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Core\CloseRequestedTests.xaml.cs">
<DependentUpon>CloseRequestedTests.xaml</DependentUpon>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<UserControl x:Class="UITests.Shared.Windows_UI_Composition.CompositionPathTests"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.Shared.Windows_UI_Composition"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">

<GridView SelectionMode="None" VerticalAlignment="Top" HorizontalAlignment="Left">
<!-- We need to set a background in order for ToolTip to work -->
<ContentPresenter x:Name="compPresenter1" Width="200" Height="200" ToolTipService.ToolTip="Basic Shape Visual" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter2" Width="200" Height="200" ToolTipService.ToolTip="Complex-Shape Shape Visual" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter3" Width="200" Height="200" ToolTipService.ToolTip="Multi-Shape Shape Visual" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter4" Width="200" Height="200" ToolTipService.ToolTip="Circle Path Geometry" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter5" Width="200" Height="200" ToolTipService.ToolTip="Rounded Rectangle Path Geometry" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter6" Width="200" Height="200" ToolTipService.ToolTip="Polygon Path Geometry" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter7" Width="200" Height="200" ToolTipService.ToolTip="Path Geometry Clipping" Background="Transparent"/>
<ContentPresenter x:Name="compPresenter8" Width="200" Height="200" ToolTipService.ToolTip="Geometry Trimming Animation" Background="Transparent"/>
</GridView>
</UserControl>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
<CheckBox x:Name="isForeverCheckBox" Content="Is forever?" />
<CheckBox x:Name="hasFrameAtZeroCheckBox" Content="Has a keyframe at 0.0f?" />
<muxc:NumberBox x:Name="iterationCountNumberBox" Header="Iteration count" Value="1" />
<ComboBox x:Name="easingFunctionBox" Header="Easing Function" SelectedIndex="0">
<ComboBoxItem Content="Default (Cubic Bezier)" />
<ComboBoxItem Content="Linear" />
<ComboBoxItem Content="Back" />
<ComboBoxItem Content="Bounce" />
<ComboBoxItem Content="Circle" />
<ComboBoxItem Content="Elastic" />
<ComboBoxItem Content="Exponential" />
<ComboBoxItem Content="Power" />
<ComboBoxItem Content="Sine"/>
<ComboBoxItem Content="Step"/>
</ComboBox>
<Button Content="Start" Click="OnStartAnimation" />
<Button Content="Stop" Click="OnStopAnimation" />
<Border x:Name="border" Width="100" Height="100" Background="Red" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,37 @@ public Vector3KeyFrameAnimationSample()

private void OnStartAnimation(object sender, RoutedEventArgs args)
{
var animation = _borderVisual.Compositor.CreateVector3KeyFrameAnimation();
var compositor = _borderVisual.Compositor;

CompositionEasingFunction defaultEasingFunction = compositor.CreateCubicBezierEasingFunction(new(0.41f, 0.52f), new(0.0f, 0.94f));

var easingFunction = easingFunctionBox.SelectedIndex switch
{
0 => defaultEasingFunction,
1 => CompositionEasingFunction.CreateLinearEasingFunction(compositor),
2 => CompositionEasingFunction.CreateBackEasingFunction(compositor, CompositionEasingFunctionMode.InOut, 0.9f),
3 => CompositionEasingFunction.CreateBounceEasingFunction(compositor, CompositionEasingFunctionMode.Out, 3, 2),
4 => CompositionEasingFunction.CreateCircleEasingFunction(compositor, CompositionEasingFunctionMode.InOut),
5 => CompositionEasingFunction.CreateElasticEasingFunction(compositor, CompositionEasingFunctionMode.Out, 2, 1),
6 => CompositionEasingFunction.CreateExponentialEasingFunction(compositor, CompositionEasingFunctionMode.Out, 6),
7 => CompositionEasingFunction.CreatePowerEasingFunction(compositor, CompositionEasingFunctionMode.Out, 10),
8 => CompositionEasingFunction.CreateSineEasingFunction(compositor, CompositionEasingFunctionMode.InOut),
9 => CompositionEasingFunction.CreateStepEasingFunction(compositor, 3),
_ => defaultEasingFunction
};

var animation = compositor.CreateVector3KeyFrameAnimation();
var y = _borderVisual.Offset.Y;
var maxX = _pageVisual.Size.X - _borderVisual.Size.X;
if ((bool)hasFrameAtZeroCheckBox.IsChecked)
{
animation.InsertKeyFrame(0.0f, new Vector3(0.0f, y, 0.0f));
animation.InsertKeyFrame(0.0f, new Vector3(0.0f, y, 0.0f), easingFunction);
}

animation.InsertKeyFrame(0.5f, new Vector3(maxX / 4.0f, y, 0.0f));
animation.InsertKeyFrame(0.6f, new Vector3(maxX / 4.0f, y, 0.0f));
animation.InsertKeyFrame(1.0f, new Vector3(maxX, y, 0.0f));
animation.Duration = TimeSpan.FromSeconds(1);
animation.InsertKeyFrame(0.5f, new Vector3(maxX / 4.0f, y, 0.0f), easingFunction);
animation.InsertKeyFrame(0.6f, new Vector3(maxX / 4.0f, y, 0.0f), easingFunction);
animation.InsertKeyFrame(1.0f, new Vector3(maxX, y, 0.0f), easingFunction);
animation.Duration = TimeSpan.FromSeconds(2);
animation.IterationCount = (int)iterationCountNumberBox.Value;
animation.IterationBehavior = (bool)isForeverCheckBox.IsChecked ? AnimationIterationBehavior.Forever : AnimationIterationBehavior.Count;
_borderVisual.StartAnimation("Offset", animation);
Expand Down
2 changes: 2 additions & 0 deletions src/Uno.UI.Composition/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
[assembly: InternalsVisibleTo("Uno.UI")]
[assembly: InternalsVisibleTo("Uno.UI.Wasm")]
[assembly: InternalsVisibleTo("Uno.UI.RuntimeTests")]
[assembly: InternalsVisibleTo("Uno.UI.RuntimeTests.Windows")]
[assembly: InternalsVisibleTo("Uno.UI.Tests")]
[assembly: InternalsVisibleTo("Uno.UI.Unit.Tests")]
[assembly: InternalsVisibleTo("Uno.UI.Toolkit")]
[assembly: InternalsVisibleTo("Uno.UI.Composition")]

[assembly: InternalsVisibleTo("SamplesApp")]
[assembly: InternalsVisibleTo("SamplesApp.Windows")]
[assembly: InternalsVisibleTo("SamplesApp.Droid")]
[assembly: InternalsVisibleTo("SamplesApp.macOS")]
[assembly: InternalsVisibleTo("SamplesApp.Wasm")]
Expand Down
116 changes: 116 additions & 0 deletions src/Uno.UI.Composition/Composition/AnimationController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.UI.Composition;

public partial class AnimationController
{
private CompositionObject? _ownerObject;
private string? _propertyName;
private KeyFrameAnimation? _animation;

private float? _progress;

// TODO: Support multiple KeyFrameAnimation association like on Windows
ahmed605 marked this conversation as resolved.
Show resolved Hide resolved

internal AnimationController(CompositionObject ownerObject, string propertyName, KeyFrameAnimation animation) : base(ownerObject.Compositor)
{
_ownerObject = ownerObject;
_propertyName = propertyName;
_animation = animation;

_animation.Stopped += Animation_Stopped;
}

internal AnimationController(Compositor compositor) : base(compositor) { }

internal void Initialize(CompositionObject ownerObject, string propertyName, KeyFrameAnimation animation)
{
if (_animation is not null)
{
_animation.Stopped -= Animation_Stopped;
}

_ownerObject = ownerObject;
_propertyName = propertyName;
_animation = animation;
_progress = null;

_animation.Stopped += Animation_Stopped;
}

public void Resume()
{
_progress = null;
EnsureAnimation().Resume();
}

public void Pause() => EnsureAnimation().Pause();

public float Progress
{
get => _progress is not null ? _progress.Value : EnsureAnimation().Progress;
set
{
var animation = EnsureAnimation();
_progress = value;
OnPropertyChanged(nameof(Progress), false);
_ownerObject?.SeekAnimation(animation, value);
}
}

internal override object GetAnimatableProperty(string propertyName, string subPropertyName)
{
if (propertyName.Equals(nameof(Progress), StringComparison.OrdinalIgnoreCase))
{
return Progress;
}
else
{
return base.GetAnimatableProperty(propertyName, subPropertyName);
}
}

private protected override void SetAnimatableProperty(ReadOnlySpan<char> propertyName, ReadOnlySpan<char> subPropertyName, object? propertyValue)
{
if (propertyName.Equals(nameof(Progress), StringComparison.OrdinalIgnoreCase))
{
Progress = SubPropertyHelpers.ValidateValue<float>(propertyValue);
}
else
{
base.SetAnimatableProperty(propertyName, subPropertyName, propertyValue);
}
}

private void Animation_Stopped(object? sender, EventArgs e)
{
_animation = null;
_progress = null;
}

private KeyFrameAnimation EnsureAnimation()
{
if (_ownerObject is null || _propertyName is null)
{
throw new InvalidOperationException("The AnimationController has not been associated with a target object or animation");
}

if (_animation == null)
{
_animation = _ownerObject.GetKeyFrameAnimation(_propertyName);
}

if (_animation == null)
{
throw new InvalidOperationException($"No animation is running on the target object for property {_propertyName}");
}

return _animation;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
#nullable enable

using System.Windows.Input;
using Windows.Graphics;

namespace Microsoft.UI.Composition
{
public partial class CompositionEllipseGeometry : CompositionGeometry
{
internal override IGeometrySource2D? BuildGeometry()
private SkiaGeometrySource2D? _geometrySource2D;

internal override IGeometrySource2D? BuildGeometry() => _geometrySource2D;

private SkiaGeometrySource2D? InternalBuildGeometry()
=> new SkiaGeometrySource2D(BuildEllipseGeometry(Center, Radius));

private protected override void OnPropertyChangedCore(string? propertyName, bool isSubPropertyChange)
{
if (propertyName is nameof(Center) or nameof(Radius))
{
_geometrySource2D?.Dispose();
_geometrySource2D = InternalBuildGeometry();
}

base.OnPropertyChangedCore(propertyName, isSubPropertyChange);
}

private protected override void DisposeInternal()
{
_geometrySource2D?.Dispose();
base.DisposeInternal();
}
}
}
63 changes: 34 additions & 29 deletions src/Uno.UI.Composition/Composition/CompositionGeometricClip.skia.cs
Original file line number Diff line number Diff line change
@@ -1,51 +1,56 @@
#nullable enable

using System;
using System.IO;
using SkiaSharp;
using Windows.Foundation;
using Windows.Graphics.Interop;

namespace Microsoft.UI.Composition;

partial class CompositionGeometricClip
{
private protected override Rect? GetBoundsCore(Visual visual)
{
switch (Geometry)
if (Geometry is not null)
{
case CompositionPathGeometry { Path.GeometrySource: SkiaGeometrySource2D geometrySource }:
return geometrySource.TightBounds.ToRect();

case CompositionPathGeometry cpg:
throw new InvalidOperationException($"Clipping with source {cpg.Path?.GeometrySource} is not supported");

case null:
return null;

default:
throw new InvalidOperationException($"Clipping with {Geometry} is not supported");
var geometry = Geometry.BuildGeometry();

if (geometry is SkiaGeometrySource2D geometrySource)
{
return geometrySource.Geometry.TightBounds.ToRect();
}
else
{
throw new InvalidOperationException($"Clipping with source {geometry} is not supported");
}
}

return null;
}

internal override void Apply(SKCanvas canvas, Visual visual)
{
switch (Geometry)
if (Geometry is not null)
{
case CompositionPathGeometry { Path.GeometrySource: SkiaGeometrySource2D geometrySource }:
var path = TransformMatrix.IsIdentity
? geometrySource
: geometrySource.Transform(TransformMatrix.ToSKMatrix());
path.CanvasClipPath(canvas, antialias: true);
break;

case CompositionPathGeometry cpg:
throw new InvalidOperationException($"Clipping with source {cpg.Path?.GeometrySource} is not supported");

case null:
// null is nop
break;

default:
throw new InvalidOperationException($"Clipping with {Geometry} is not supported");
var geometry = Geometry.BuildGeometry();

if (geometry is SkiaGeometrySource2D geometrySource)
{
var path = geometrySource.Geometry;
if (!TransformMatrix.IsIdentity)
{
var transformedPath = new SKPath();
path.Transform(TransformMatrix.ToSKMatrix(), transformedPath);
path = transformedPath;
}

canvas.ClipPath(path, antialias: true);
}
else
{
throw new InvalidOperationException($"Clipping with source {geometry} is not supported");
}
}
}
}
Loading
Loading