diff --git a/src/Beutl.Engine/Graphics/FilterEffects/PathFollowEffect.cs b/src/Beutl.Engine/Graphics/FilterEffects/PathFollowEffect.cs new file mode 100644 index 000000000..b1aa15974 --- /dev/null +++ b/src/Beutl.Engine/Graphics/FilterEffects/PathFollowEffect.cs @@ -0,0 +1,96 @@ +using System.ComponentModel.DataAnnotations; +using Beutl.Engine; +using Beutl.Language; +using Beutl.Media; +using SkiaSharp; + +namespace Beutl.Graphics.Effects; + +public sealed partial class PathFollowEffect : FilterEffect +{ + public PathFollowEffect() + { + ScanProperties(); + Geometry.CurrentValue = new PathGeometry(); + } + + [Display(Name = nameof(Strings.Geometry), ResourceType = typeof(Strings))] + public IProperty Geometry { get; } = Property.Create(); + + [Display(Name = nameof(Strings.Progress), ResourceType = typeof(Strings))] + public IProperty Progress { get; } = Property.CreateAnimatable(0f); + + [Display(Name = nameof(Strings.FollowRotation), ResourceType = typeof(Strings))] + public IProperty FollowRotation { get; } = Property.CreateAnimatable(false); + + public override void ApplyTo(FilterEffectContext context, FilterEffect.Resource resource) + { + var r = (Resource)resource; + if (r.Geometry == null) + return; + + SKPath skPath = r.Geometry.GetCachedPath(); + if (skPath.IsEmpty) + return; + + float progress = Math.Clamp(r.Progress, 0f, 100f) / 100f; + + using var pathMeasure = new SKPathMeasure(skPath); + float totalLength = pathMeasure.Length; + if (totalLength <= 0) + return; + + float distance = totalLength * progress; + + if (!pathMeasure.GetPositionAndTangent(distance, out SKPoint position, out SKPoint tangent)) + return; + + if (!pathMeasure.GetPosition(0, out SKPoint startPosition)) + return; + + float offsetX = position.X - startPosition.X; + float offsetY = position.Y - startPosition.Y; + + float rotationAngle = 0f; + if (r.FollowRotation) + { + rotationAngle = MathF.Atan2(tangent.Y, tangent.X); + } + + context.CustomEffect((offsetX, offsetY, rotationAngle), static (data, effectContext) => + { + effectContext.ForEach((_, target) => + { + + var translate = Matrix.CreateTranslation(data.offsetX, data.offsetY); + Matrix m1, m2; + if (data.rotationAngle != 0) + { + var center = new Vector(target.Bounds.Width / 2, target.Bounds.Height / 2); + var rotate = Matrix.CreateRotation(data.rotationAngle); + + var offset1 = Matrix.CreateTranslation(center + target.Bounds.Position); + var offset2 = Matrix.CreateTranslation(center); + m1 = -offset1 * rotate * offset1 * translate; + m2 = -offset2 * rotate * offset2 * translate; + } + else + { + m1 = m2 = translate; + } + + var newBounds = target.Bounds.TransformToAABB(m1); + var newTarget = effectContext.CreateTarget(newBounds); + using (var canvas = effectContext.Open(newTarget)) + using (canvas.PushTransform(Matrix.CreateTranslation(target.Bounds.Position - newTarget.Bounds.Position))) + using (canvas.PushTransform(m2)) + { + target.Draw(canvas); + } + + target.Dispose(); + return newTarget; + }); + }); + } +} diff --git a/src/Beutl.Language/Strings.Designer.cs b/src/Beutl.Language/Strings.Designer.cs index 4c07eb72f..018238445 100644 --- a/src/Beutl.Language/Strings.Designer.cs +++ b/src/Beutl.Language/Strings.Designer.cs @@ -2630,5 +2630,29 @@ public static string Convert_to_preset { return ResourceManager.GetString("Convert_to_preset", resourceCulture); } } + + public static string Geometry { + get { + return ResourceManager.GetString("Geometry", resourceCulture); + } + } + + public static string Progress { + get { + return ResourceManager.GetString("Progress", resourceCulture); + } + } + + public static string FollowRotation { + get { + return ResourceManager.GetString("FollowRotation", resourceCulture); + } + } + + public static string PathFollowEffect { + get { + return ResourceManager.GetString("PathFollowEffect", resourceCulture); + } + } } } diff --git a/src/Beutl.Language/Strings.ja.resx b/src/Beutl.Language/Strings.ja.resx index 580dd389b..6ed4cf489 100644 --- a/src/Beutl.Language/Strings.ja.resx +++ b/src/Beutl.Language/Strings.ja.resx @@ -1415,4 +1415,16 @@ プリセットに変換 + + ジオメトリ + + + 進捗 + + + 回転に追従 + + + パス追従 + diff --git a/src/Beutl.Language/Strings.resx b/src/Beutl.Language/Strings.resx index 967ef5758..0e172689d 100644 --- a/src/Beutl.Language/Strings.resx +++ b/src/Beutl.Language/Strings.resx @@ -1410,4 +1410,16 @@ Convert to preset + + Geometry + + + Progress + + + Follow Rotation + + + Path Follow + diff --git a/src/Beutl.Operators/LibraryRegistrar.cs b/src/Beutl.Operators/LibraryRegistrar.cs index 2ea3c7c9c..a5e12f2d4 100644 --- a/src/Beutl.Operators/LibraryRegistrar.cs +++ b/src/Beutl.Operators/LibraryRegistrar.cs @@ -145,6 +145,7 @@ public static void RegisterAll() .AddFilterEffect(Strings.ColorShift) .AddFilterEffect(Strings.ShakeEffect) .AddFilterEffect(Strings.DisplacementMap) + .AddFilterEffect(Strings.PathFollowEffect) .AddFilterEffect("Layer") .AddGroup("OpenCV", gg => gg .AddFilterEffect("CvBlur") diff --git a/src/Beutl/Views/Editors/GeometryEditor.axaml b/src/Beutl/Views/Editors/GeometryEditor.axaml index 04fddaa0c..f37ba156d 100644 --- a/src/Beutl/Views/Editors/GeometryEditor.axaml +++ b/src/Beutl/Views/Editors/GeometryEditor.axaml @@ -24,6 +24,9 @@ + diff --git a/src/Beutl/Views/Editors/GeometryEditor.axaml.cs b/src/Beutl/Views/Editors/GeometryEditor.axaml.cs index 3f330b04a..12c985f6a 100644 --- a/src/Beutl/Views/Editors/GeometryEditor.axaml.cs +++ b/src/Beutl/Views/Editors/GeometryEditor.axaml.cs @@ -60,7 +60,7 @@ private void Tag_Click(object? sender, RoutedEventArgs e) else { _logger.LogInformation("Group is not selected, showing context flyout."); - //expandToggle.ContextFlyout?.ShowAt(expandToggle); + expandToggle.ContextFlyout?.ShowAt(expandToggle); } } @@ -113,25 +113,6 @@ private async void ImportFromSvgPathClick(object? sender, RoutedEventArgs e) } } - private void AddClick(object? sender, RoutedEventArgs e) - { - if (DataContext is GeometryEditorViewModel { IsDisposed: false } viewModel - && sender is MenuFlyoutItem item - && viewModel.IsGroup.Value) - { - try - { - _logger.LogInformation("Adding item from menu flyout."); - viewModel.AddItem(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error occurred while adding item from menu flyout."); - NotificationService.ShowError("Error", ex.Message); - } - } - } - private void SetNullClick(object? sender, RoutedEventArgs e) { if (DataContext is not GeometryEditorViewModel { IsDisposed: false } viewModel) return; @@ -139,4 +120,12 @@ private void SetNullClick(object? sender, RoutedEventArgs e) _logger.LogInformation("Setting value to null."); viewModel.SetNull(); } + + private void InitializeClick(object? sender, RoutedEventArgs e) + { + if (DataContext is not GeometryEditorViewModel { IsDisposed: false } viewModel) return; + + _logger.LogInformation("Initializing geometry type."); + viewModel.ChangeGeometryType(typeof(PathGeometry)); + } }