在 haskell 中接受 4 个参数的贝塞尔函数

a bezier function that takes 4 args in haskell

我正在尝试制作一个接受 4 个参数的贝塞尔函数:

> import Diagrams.Backend.SVG.CmdLine
> import Diagrams.Prelude
> import Control.Applicative

> bezier4 x1 c1 c2 x2 = bezier3 (c1 ^-^ x1) (c2 ^-^ x1) (x2 ^-^ x1) # translate x1
> lineBtwPoints p1 p2 = fromOffsets [p2 ^-^ p1] # translate p1
> illustrateBézier x1 c1 c2 x2
>     =  endpt  # translate x1
>     <> endpt  # translate x2
>     <> ctrlpt # translate c1
>     <> ctrlpt # translate c2
>     <> l1
>     <> l2
>     <> fromSegments [bezier4 x1 c1 c2 x2]
>   where
>     dashed  = dashingN [0.03,0.03] 0
>     endpt   = circle 0.05 # fc red  # lw none
>     ctrlpt  = circle 0.05 # fc blue # lw none
>     l1      = lineBtwPoints x1 c1 # dashed
>     l2      =  lineBtwPoints x2 c2 # dashed
>
> x1      = r2 (0.3, 0.5) :: R2
> x2      = r2 (3,-1) :: R2         -- endpoint
> [c1,c2] = map r2 [(1,2), (3,0)]   -- control points
> example = illustrateBézier x1 c1 c2 x2

但是结果好像不是我想要的:

首先让我们解决这个名字。通常 bezier4 是给出四次贝塞尔曲线段的函数的名称。更好的名称是 fixedBezier3,更好的形式是采用 Points 而不是向量作为参数。实际上,此函数作为 FixedSegment 数据类型的 FCubic 存在。

如果我们查看 bezier4 的类型,我们可以看出哪里出了问题:

bezier4 x1 c1 c2 x2 = bezier3 (c1 ^-^ x1) (c2 ^-^ x1) (x2 ^-^ x1) # translate x1

ghci> :t bezier4
bezier4'
  :: (Data.Basis.HasBasis v,
      Data.MemoTrie.HasTrie (Data.Basis.Basis v)) =>
     v -> v -> v -> v -> Segment Closed v

重要的是结果是 Segment Closed v。阅读 Segmentdocumentation

Segments are translationally invariant, that is, they have no particular "location" and are unaffected by translations. They are, however, affected by other transformations such as rotations and scales.

bezier4 结尾的翻译不会有任何效果,因为类型 Segment 不能表示具有位置的值,它只表示 "shape" 和位移。我们可以在 GHCi 中看到:

ghci> bezier4 x1 c1 c2 x2
Cubic (0.7 ^& 1.5) (2.7 ^& (-0.5)) (OffsetClosed (2.7 ^& (-1.5)))
ghci> bezier4' x1 c1 c2 x2 # translate (r2 (1000,1000))
Cubic (0.7 ^& 1.5) (2.7 ^& (-0.5)) (OffsetClosed (2.7 ^& (-1.5)))

一个修复方法是使 bezier4 的类型产生 Located (Segment Closed v)。通过这种类型,我们至少可以表达出想要的曲线:

bezier4' x1 c1 c2 x2 = bezier3 (c1 ^-^ x1) (c2 ^-^ x1) (x2 ^-^ x1) `at` (0 .+^ x1)

ghci> bezier4' x1 c1 c2 x2
Loc { loc = P (0.3 ^& 0.5)
    , unLoc = Cubic (0.7 ^& 1.5) (2.7 ^& (-0.5)) (OffsetClosed (2.7 ^& (-1.5)))
    }

请注意,我们获得了与之前相同的段,但现在我们有了一个位置。

ghci> bezier4' x1 c1 c2 x2 # translate (r2 (1000,1000))
Loc { loc = P (1000.3 ^& 1000.5)
    , unLoc = Cubic (0.7 ^& 1.5) (2.7 ^& (-0.5)) (OffsetClosed (2.7 ^& (-1.5)))
    }

虽然我们在这一点上有点卡住了。定位的段并不是特别有趣,因为通常我们希望将许多段串在一起作为 Trail。定位到的 list 段让我们到达那里,我们可以使用 fromLocSegments:

fromLocSegments :: TrailLike t => Located [Segment Closed (V t)] -> t

现在我们有了一些可以使用的东西(在 bezier4 的使用站点进行了额外的更改):

bezier4 x1 c1 c2 x2 = fromSegments [bezier3 (c1 ^-^ x1) (c2 ^-^ x1) (x2 ^-^ x1)]
                    # translate x1

请注意,我们不能将此函数的输出与其他段串在一起以形成更长的轨迹。 Diagrams 选择使用具有 SegmentTrailLocatedPath 的强类型,只允许与输出中表达的内容精确匹配的值("meaning") .例如,假设我们要写 fromFixedSegments:

fromFixedSegments :: TrailLike t => [FizedSegment (V t)] -> t

每个立方体线段有四个点,但结果将是一条具有 "no gaps" 含义的轨迹。为此,我们将不得不丢弃相邻段的第一个或最后一个点的信息。这里没有好的选择!