Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 96 additions & 0 deletions src/Beutl.Engine/Graphics/FilterEffects/PathFollowEffect.cs
Original file line number Diff line number Diff line change
@@ -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<PathFollowEffect>();
Geometry.CurrentValue = new PathGeometry();
}

[Display(Name = nameof(Strings.Geometry), ResourceType = typeof(Strings))]
public IProperty<Geometry?> Geometry { get; } = Property.Create<Geometry?>();

[Display(Name = nameof(Strings.Progress), ResourceType = typeof(Strings))]
public IProperty<float> Progress { get; } = Property.CreateAnimatable(0f);

[Display(Name = nameof(Strings.FollowRotation), ResourceType = typeof(Strings))]
public IProperty<bool> 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;
});
});
}
}
24 changes: 24 additions & 0 deletions src/Beutl.Language/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/Beutl.Language/Strings.ja.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1415,4 +1415,16 @@
<data name="Convert_to_preset" xml:space="preserve">
<value>プリセットに変換</value>
</data>
<data name="Geometry" xml:space="preserve">
<value>ジオメトリ</value>
</data>
<data name="Progress" xml:space="preserve">
<value>進捗</value>
</data>
<data name="FollowRotation" xml:space="preserve">
<value>回転に追従</value>
</data>
<data name="PathFollowEffect" xml:space="preserve">
<value>パス追従</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/Beutl.Language/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1410,4 +1410,16 @@
<data name="Convert_to_preset" xml:space="preserve">
<value>Convert to preset</value>
</data>
<data name="Geometry" xml:space="preserve">
<value>Geometry</value>
</data>
<data name="Progress" xml:space="preserve">
<value>Progress</value>
</data>
<data name="FollowRotation" xml:space="preserve">
<value>Follow Rotation</value>
</data>
<data name="PathFollowEffect" xml:space="preserve">
<value>Path Follow</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/Beutl.Operators/LibraryRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ public static void RegisterAll()
.AddFilterEffect<ColorShift>(Strings.ColorShift)
.AddFilterEffect<ShakeEffect>(Strings.ShakeEffect)
.AddFilterEffect<DisplacementMapEffect>(Strings.DisplacementMap)
.AddFilterEffect<PathFollowEffect>(Strings.PathFollowEffect)
.AddFilterEffect<LayerEffect>("Layer")
.AddGroup("OpenCV", gg => gg
.AddFilterEffect<Graphics.Effects.OpenCv.Blur>("CvBlur")
Expand Down
3 changes: 3 additions & 0 deletions src/Beutl/Views/Editors/GeometryEditor.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<ui:MenuFlyoutItem Click="SetNullClick"
FontWeight="Normal"
Text="Null" />
<ui:MenuFlyoutItem Click="InitializeClick"
FontWeight="Normal"
Text="{x:Static lang:Strings.Initialize}" />
<ui:MenuFlyoutItem Click="ImportFromSvgPathClick"
FontWeight="Normal"
Text="{x:Static lang:Strings.ImportSvgPath}" />
Expand Down
29 changes: 9 additions & 20 deletions src/Beutl/Views/Editors/GeometryEditor.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -113,30 +113,19 @@ 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;

_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));
}
}
Loading