F#でWPF --- CatmullRom曲線の描画
図形を描画する際に、指定した点を通る滑らかな曲線を描きたいことがあります。
今回はCatmullRom曲線でこれを実現したいと思います。
CatmullRom曲線
2点間の補間は、更にその前後の点を利用して求められます。
2点(p1、p2)と、その前後の点(p0、p3)の4点から、下記の式にて任意の位置が求められます。
Vector2定義
System.Windows.Vectorでは演算子が不十分であるため、独自のVector2を下記の様に定義しました。
type Vector2 = { X:double; Y:double } static member (~-) (v:Vector2) = { X = -v.X; Y = -v.Y } static member (+) (a:Vector2, b:Vector2) = { X = a.X + b.X; Y = a.Y + b.Y } static member (-) (a:Vector2, b:Vector2) = { X = a.X - b.X; Y = a.Y - b.Y } static member (*) (s:double, v:Vector2) = { X = s * v.X; Y = s * v.Y } static member (*) (v:Vector2, s:double) = s * v static member (/) (v:Vector2, s:double) = { X = v.X / s; Y = v.Y / s }
CatmullRom定義
Vector2 listを持つレコードと、interpolate関数を定義しています。
type CatmullRom = { Points : Vector2 list } [<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>] module CatmullRom = let interpolate (p0 : Vector2) (p1 : Vector2) (p2 : Vector2) (p3 : Vector2) t = let v0 = (p2 - p0) / 2.0 let v1 = (p3 - p1) / 2.0 (2.0 * p1 - 2.0 * p2 + v0 + v1) * t * t * t + (-3.0 * p1 + 3.0 * p2 - 2.0 * v0 - v1) * t * t + v0 * t + p1
Polylineで表示
CatmullRom曲線をPolylineで表示してみます。
4つの点の間を100分割して表示しています。(始点、終点は複製をしてあるため、入力は6点となっています。)
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="MainWindow" Height="180" Width="160"> <Canvas Margin="20"> <Polyline Stroke="Red" StrokeThickness="10" Points="{Binding Points}" /> </Canvas> </Window>
open System open System.Windows open System.Windows.Media type Model = { Points : PointCollection } [<STAThread>] [<EntryPoint>] let main argv = let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window let interpolate p0 p1 p2 p3 count = [ 0..count - 1 ] |> List.map (fun x -> double x / double count) |> List.map (CatmullRom.interpolate p0 p1 p2 p3) let points = [ { X = 0.0; Y = 0.0 } { X = 0.0; Y = 0.0 } { X = 100.0; Y = 0.0 } { X = 0.0; Y = 100.0 } { X = 100.0; Y = 100.0 } { X = 100.0; Y = 100.0 } ] |> List.windowed 4 |> List.collect (fun x -> interpolate x.[0] x.[1] x.[2] x.[3] 100) let toPoint x = Point(x.X, x.Y) window.DataContext <- { Points = points |> List.map toPoint |> PointCollection } Application().Run(window) |> ignore 0
Pathで描画
スタート位置をPathFigureで指定し、残りをPolyBezierSegmentのPointsにバインドすることによりベジエ曲線を描いています。
Pointsの中身は(p1、c1_1、c2_2、p2、c2_1、c2_2、p3...)のように、通過点の間にコントロールポイントが挟まっているものになります。
Polylineで描画したものと同じ図形であることが確認できます。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="MainWindow" Height="180" Width="160"> <Canvas Margin="20"> <Path Stroke="Red" StrokeThickness="10"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="{Binding BezierStartPoint}"> <PolyBezierSegment Points="{Binding BezierPoints}" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Canvas> </Window>
module CatmullRom = let bezierControlPoints (p0 : Vector2) (p1 : Vector2) (p2 : Vector2) (p3 : Vector2) = let c1 = p1 + (p2 - p0) / 6.0 let c2 = p2 + (p1 - p3) / 6.0 c1, c2 type Model = { BezierStartPoint : Point BezierPoints : PointCollection } [<STAThread>] [<EntryPoint>] let main argv = let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window let points = [ { X = 0.0; Y = 0.0 } { X = 0.0; Y = 0.0 } { X = 100.0; Y = 0.0 } { X = 0.0; Y = 100.0 } { X = 100.0; Y = 100.0 } { X = 100.0; Y = 100.0 } ] |> List.windowed 4 |> List.collect (fun x -> let c1, c2 = CatmullRom.bezierControlPoints x.[0] x.[1] x.[2] x.[3] [ c1; c2; x.[2] ]) let toPoint x = Point(x.X, x.Y) window.DataContext <- { BezierStartPoint = Point(0.0, 0.0) BezierPoints = points |> List.map toPoint |> PointCollection } Application().Run(window) |> ignore 0