I am trying to make a bezier function that takes 4 args:
> 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
But the result seems not to be what I wanted:
First lets address the name. Typically bezier4
would be the name for a function giving a quartic bezier curve segment. A better name would be fixedBezier3
, and a better form would be to take Points
rather then vectors for the arguments. Indeed this function exists as FCubic
from the FixedSegment
data type.
If we look at the type of bezier4
we can see where things go wrong:
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
The important part is that the result is a Segment Closed v
. Reading the documentation for Segment
:
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.
The translation at the end of bezier4
will not have any effect as the type Segment
cannot express values that have a location, it just expresses the "shape" and displacement. We can see this in 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)))
One fix would be to make the type of bezier4
result in a Located (Segment Closed v)
. With this type we can at least express the curve wanted:
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)))
}
Notice we get the same segment as before, but now we have a location.
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)))
}
We are a bit stuck at this point though. Located segments are not particularly interesting as typically we want to string together many segments as a Trail
. A located list of segments gets us there and we can use fromLocSegments
:
fromLocSegments :: TrailLike t => Located [Segment Closed (V t)] -> t
Now we have something that will work (with an additional change at the use site of bezier4
):
bezier4 x1 c1 c2 x2 = fromSegments [bezier3 (c1 ^-^ x1) (c2 ^-^ x1) (x2 ^-^ x1)]
# translate x1
Note that we cannot string together the output of this function with other segments to make longer trails. Diagrams chooses to use strong types with Segment
, Trail
, Located
, and Path
only allow values that precisely match what is expressed in output (the "meaning"). For instance, say we want to write
fromFixedSegments
:
fromFixedSegments :: TrailLike t => [FizedSegment (V t)] -> t
Each cubic segment would have four points, but the result is going to be a trail having the meaning of "no gaps". To do this we will have to throw away the information of the first or last points of adjacent segments. There is no good choice here!