WPF、図形の回転、PathGeometryで描画した図形の中心は全頂点の平均座標もいいね
違和感ZERO
結果
ユーチューブ
youtu.be
GIFアニメーション

赤は前回の方法、全頂点が収まる矩形の中心で回転
前回のもいいけど今回のピンクのほうもいいねえ!
エクセルの図形の回転は
前回の方法と同じだと思われる
二等辺三角形で試すとよく分かる

テストアプリでも確認

GIFアニメーション

テストアプリのコード
2025WPF/20250130_CenterRotateEzLine at main · gogowaten/2025WPF
github.com
Class.cs
using System.Globalization; using System.Windows.Data; using System.Windows.Media; using System.Windows.Shapes; using System.Windows; namespace _20250130_CenterRotateEzLine { class Class1 { } public class EzLine : Shape { public EzLine() { MultiBinding mb = new() { Converter = new MyConverterRotateTF() }; mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyIsRotateCenterAverageProperty), Mode = BindingMode.OneWay }); mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyAngleProperty), Mode = BindingMode.OneWay }); mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyRectProperty), Mode = BindingMode.OneWay }); mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(MyPointsProperty), Mode = BindingMode.OneWay }); SetBinding(RenderTransformProperty, mb); } protected override Geometry DefiningGeometry { get { if (MyPoints.Count == 0) { return Geometry.Empty; } StreamGeometry geo = new(); using (var context = geo.Open()) { context.BeginFigure(MyPoints[0], isFilled: MyIsFilled, isClosed: MyIsClosed); PointCollection pc = new(MyPoints.Clone()); pc.RemoveAt(0); context.PolyLineTo(pc, isStroked: MyIsStroked, isSmoothJoin: MyIsSmoothJoin); } geo.Freeze(); return geo; } } //描画直後に //全頂点が収まる矩形を取得 protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); MyBounds = RenderedGeometry.Bounds; } #region 依存関係プロパティ public FillRule MyFillRule { get { return (FillRule)GetValue(MyFillRuleProperty); } set { SetValue(MyFillRuleProperty, value); } } public static readonly DependencyProperty MyFillRuleProperty = DependencyProperty.Register(nameof(MyFillRule), typeof(FillRule), typeof(EzLine), new FrameworkPropertyMetadata(FillRule.Nonzero, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public bool MyIsSmoothJoin { get { return (bool)GetValue(MyIsSmoothJoinProperty); } set { SetValue(MyIsSmoothJoinProperty, value); } } public static readonly DependencyProperty MyIsSmoothJoinProperty = DependencyProperty.Register(nameof(MyIsSmoothJoin), typeof(bool), typeof(EzLine), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public bool MyIsFilled { get { return (bool)GetValue(MyIsFilledProperty); } set { SetValue(MyIsFilledProperty, value); } } public static readonly DependencyProperty MyIsFilledProperty = DependencyProperty.Register(nameof(MyIsFilled), typeof(bool), typeof(EzLine), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public bool MyIsClosed { get { return (bool)GetValue(MyIsClosedProperty); } set { SetValue(MyIsClosedProperty, value); } } public static readonly DependencyProperty MyIsClosedProperty = DependencyProperty.Register(nameof(MyIsClosed), typeof(bool), typeof(EzLine), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public bool MyIsStroked { get { return (bool)GetValue(MyIsStrokedProperty); } set { SetValue(MyIsStrokedProperty, value); } } public static readonly DependencyProperty MyIsStrokedProperty = DependencyProperty.Register(nameof(MyIsStroked), typeof(bool), typeof(EzLine), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); #endregion 依存関係プロパティ #region 必須依存関係プロパティ //回転の中心の指定 //trueで平均座標 //falseで全頂点が収まる矩形の中心 public bool MyIsRotateCenterAverage { get { return (bool)GetValue(MyIsRotateCenterAverageProperty); } set { SetValue(MyIsRotateCenterAverageProperty, value); } } public static readonly DependencyProperty MyIsRotateCenterAverageProperty = DependencyProperty.Register(nameof(MyIsRotateCenterAverage), typeof(bool), typeof(EzLine), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public double MyAngle { get { return (double)GetValue(MyAngleProperty); } set { SetValue(MyAngleProperty, value); } } public static readonly DependencyProperty MyAngleProperty = DependencyProperty.Register(nameof(MyAngle), typeof(double), typeof(EzLine), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); //全頂点が収まる矩形を保持 private static readonly DependencyPropertyKey MyBoundsPropertyKey = DependencyProperty.RegisterReadOnly(nameof(MyBounds), typeof(Rect), typeof(EzLine), new PropertyMetadata(new Rect())); public static readonly DependencyProperty MyRectProperty = MyBoundsPropertyKey.DependencyProperty; public Rect MyBounds { get { return (Rect)GetValue(MyBoundsPropertyKey.DependencyProperty); } internal set { SetValue(MyBoundsPropertyKey, value); } } public PointCollection MyPoints { get { return (PointCollection)GetValue(MyPointsProperty); } set { SetValue(MyPointsProperty, value); } } public static readonly DependencyProperty MyPointsProperty = DependencyProperty.Register(nameof(MyPoints), typeof(PointCollection), typeof(EzLine), new FrameworkPropertyMetadata(new PointCollection(), FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); #endregion 必須依存関係プロパティ } //回転の中心座標を計算して //RotateTransformに変換 public class MyConverterRotateTF : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var isCenterAverage = (bool)values[0]; var angle = (double)values[1]; var r = (Rect)values[2]; var pc = (PointCollection)values[3]; Point centerP; if (isCenterAverage) { //平均座標 double x = 0, y = 0; foreach (var item in pc) { x += item.X; y += item.Y; } centerP = new(x / pc.Count, y / pc.Count); } else { //全頂点が収まる矩形の中心 centerP = new Point(r.Width / 2.0, r.Height / 2.0); } return new RotateTransform(angle, centerP.X, centerP.Y); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
MainWindow.xaml
<Window x:Class="_20250130_CenterRotateEzLine.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 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" xmlns:local="clr-namespace:_20250130_CenterRotateEzLine" mc:Ignorable="d" Title="絶対回転黙示録" Height="367" Width="602"> <Window.Resources> <Storyboard x:Key="anime"> <DoubleAnimation Storyboard.TargetName="MyAngle" Storyboard.TargetProperty="Value" From="0" To="360" Duration="0:0:5" RepeatBehavior="0:0:10" /> </Storyboard> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="200"/> </Grid.ColumnDefinitions> <Canvas> <local:EzLine Opacity="0.5" Canvas.Left="50" Canvas.Top="80" MyIsRotateCenterAverage="True" MyPoints="0,0, 100,50, 100,200, 200,0, 10,150, 100,0" Stroke="Pink" StrokeThickness="{Binding ElementName=slStrokeThickness, Path=Value}" Fill="MistyRose" MyIsFilled="{Binding ElementName=toggIsFilled, Path=IsChecked}" MyIsClosed="True"/> <local:EzLine x:Name="tenjo" Opacity="0.5" Canvas.Left="50" Canvas.Top="80" MyIsRotateCenterAverage="True" MyPoints="0,0, 100,50, 100,200, 200,0, 10,150, 100,0" Stroke="Fuchsia" StrokeThickness="{Binding ElementName=slStrokeThickness, Path=Value}" MyAngle="{Binding ElementName=MyAngle, Path=Value}" Fill="MistyRose" MyIsFilled="{Binding ElementName=toggIsFilled, Path=IsChecked}" MyIsClosed="True"/> <local:EzLine Opacity="0.5" Canvas.Left="250" Canvas.Top="80" MyIsRotateCenterAverage="True" MyPoints="0,0, 100,50, 100,200, 200,0, 10,150, 100,0" Stroke="Pink" Fill="MistyRose" StrokeThickness="{Binding ElementName=slStrokeThickness, Path=Value}" MyIsFilled="{Binding ElementName=toggIsFilled, Path=IsChecked}" MyIsClosed="True"/> <local:EzLine x:Name="himemiya" Opacity="0.5" Canvas.Left="250" Canvas.Top="80" MyIsRotateCenterAverage="False" MyPoints="0,0, 100,50, 100,200, 200,0, 10,150, 100,0" Stroke="Red" Fill="MistyRose" StrokeThickness="{Binding ElementName=slStrokeThickness, Path=Value}" MyAngle="{Binding ElementName=MyAngle, Path=Value}" MyIsFilled="{Binding ElementName=toggIsFilled, Path=IsChecked}" MyIsClosed="True"/> <Ellipse Width="10" Height="10" Canvas.Left="216.6" Canvas.Top="150"/> </Canvas> <StackPanel Grid.Column="1"> <TextBlock Text="全頂点の平均座標で回転" FontSize="24" Foreground="Fuchsia" TextWrapping="Wrap"/> <TextBlock Text="図形が収まる矩形の中心で回転" FontSize="24" TextWrapping="Wrap" Foreground="Red"/> <TextBlock Text="{Binding ElementName=MyAngle, Path=Value, StringFormat={}{0:000} 回転角度}"/> <Slider x:Name="MyAngle" Minimum="0" Maximum="360" TickFrequency="5" IsSnapToTickEnabled="True" Margin="10"/> <TextBlock Text="{Binding ElementName=slStrokeThickness, Path=Value, StringFormat={}{0:000} 線の太さ}"/> <Slider x:Name="slStrokeThickness" Value="20" Minimum="0" Maximum="50" TickFrequency="5" IsSnapToTickEnabled="True" Margin="10"/> <ToggleButton x:Name="toggIsFilled" Content="IsFilled"/> <Button Content="anime"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard Storyboard="{StaticResource anime}"/> </EventTrigger> </Button.Triggers> </Button> </StackPanel> </Grid> </Window>
今思ったけど、4つのEzLineのプロパティの値はほとんど同じだから、Styleを使えば効率よく書けたはず
MainWindow.xaml.cs
using System.Windows; namespace _20250130_CenterRotateEzLine { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
参照したところ
【高校数学】”三角形の重心”の公式とその証明 | enggy
enggy.net
WPF4.5入門 その48 「WPFのアニメーション その1」 - かずきのBlog@hatena
blog.okazuki.jp
感想

図形の中心で検索したら、いくつか方法というか考え方があって、その中でも「重心」ってのが良さそうだったんだけど、計算方法が複雑すぎて諦めた。それでも三角形の重心なら、計算方法は座標の平均値だったので、それをそのまま使ってみたらうまくいった。重心とは違う座標になるんだけど、回転している様子を見る限り完璧
WPFでのアニメーションと録画ファイルサイズ
初めて使ってみた、楽しい
ただ、動画ファイルはサイズがかなり大きくなる。綺麗にアニメーションするのでいつもは30fpsで録画しているのを60fpsにしたら、今までは0.5MB程度だったのが、今回のは60fpsのせいもあるけど7.5MBと10倍以上!
大きすぎるのでGIFは20fpsに変換して3.3MBを載せてみた。ユーチューブのほうはそのまま60fpsをx264に変換して44MBと、こちらはいつもと同じくらい。あと色のフォーマットをi444を使ってみた。ユーチューブでは再変換されるけどi420からi420よりはi444からi420のほうが色のにじみが軽減される?
録画サイズを抑えるには角度変更をリニアじゃなくて、ステップで10度づつとかで変更できればいいんだけど、そういうメソッドやプロパティはないみたいなので、フレームレートを落とすしかなさそうねえ
関連記事
次のWPF記事
WPF、中のTextBlock(子要素)を回転させたときに、自身(親要素)のサイズと位置を違和感なく変更できた - 午後わてんのブログ
gogowaten.hatenablog.com
前回のWPF記事
WPF、図形の回転、PathGeometryで描画した図形の「中央」を中心に回転させるには - 午後わてんのブログ
gogowaten.hatenablog.com