午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、図形の回転、PathGeometryで描画した図形の中心は全頂点の平均座標もいいね

違和感ZERO

結果

ユーチューブ
youtu.be

GIFアニメーション

Animation20250131_120353_20fps.gif
ピンクが全頂点の平均座標を回転の中心として回転
赤は前回の方法、全頂点が収まる矩形の中心で回転

前回のもいいけど今回のピンクのほうもいいねえ!



エクセルの図形の回転は

前回の方法と同じだと思われる
二等辺三角形で試すとよく分かる

エクセル2007

テストアプリでも確認
テストアプリ

GIFアニメーション

Animation20250131_125729.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