packages feed

moonlight-planar-1.2.0.0: docs/art-common/Moonlight/Planar/Exhibit/EquationalGarden.hs

-- | A moonlit botanical plate authored with profiles, interpolated stations,
-- bowed lines and reusable local frames. No illustration-owned curve handles.
module Moonlight.Planar.Exhibit.EquationalGarden
  ( GardenControls (..)
  , defaultGardenControls
  , GardenPart (..)
  , gardenPartName
  , gardenPicture
  , GardenAttachmentError (..)
  , GardenPlant (..)
  , StemPort (..), LeafPort (..), BlossomPort (..)
  , gardenPlants, gardenLayoutPicture
  , leafMotif
  , petalOutline
  , leafOutline
  ) where

import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Sequence as Seq
import Data.Bifunctor (first)
import Moonlight.Planar.Affine
  ( Affine2, AffineIso2, affine2, affineIso2, affineIsoMap, composeAffineIso2
  , identityAffineIso2, translationAffineIso2 )
import Moonlight.Planar.Curve
  ( ClosedTrail, Located, OpenTrail, Subpath (..), circle, ellipse, path
  , locate, openTrail, curveStep, line, transformLocatedClosedTrail )
import Moonlight.Planar.Curve.Authoring
  ( Knot (..), ProfileStation (..), bowedTrail, cardinalOpen, polygonTrail, profileOutline )
import Moonlight.Planar.Curve.Frame
  ( FrameError, FramePolicy, FrameSite (..), exactTrailSite, framePolicy
  , measuredFrameIso, regularFrame )
import Moonlight.Planar.Curve.Measure (RadicalPrecisionError, radicalPrecision)
import Moonlight.Planar.Exact
  ( ExactPoint, ExactRational, ExactVector (..), PositiveExact, ScalarRefinementError
  , UnitInterval, exactHalf, exactThird, exactPoint, exactPointCoordinates, positiveExact
  , positiveOne, positiveTwo, unitHalf, unitOne, unitZero, unitIntervalValue
  , boundsMinimumX, boundsMaximumX, boundsMinimumY, boundsMaximumY )
import Moonlight.Planar.Illustration
  ( Color (..), FillRule (..), GradientStop (..), LineCap (..), LineJoin (..)
  , Paint (..), Picture, StrokeStyle (..), StrokeUnits (..)
  , annotate, clip, fill, gradientStops, opacity, place, stroke )
import Moonlight.Planar.Illustration.Motif
  ( Motif, motif, motifPicture, motifPort, attachMotif, transformMotif )
import Moonlight.Planar.Illustration.Layout
  ( LayoutAxis (..), alignMotif, arrangeMotifs, geometryEnvelope, geometryBounds )

-- | Ordinary semantic controls: opening changes petals; breeze bends stems
-- and leaves. The unit interval is admitted once by its existing scalar owner.
data GardenControls = GardenControls
  { blossomOpening :: !UnitInterval
  , gardenBreeze :: !UnitInterval
  } deriving stock (Eq, Show)

defaultGardenControls :: GardenControls
defaultGardenControls = GardenControls unitHalf unitHalf

data GardenPart = NightSky | Moon | Ground | Stems | Leaves | Blossoms | Ribbon | Fireflies
  deriving stock (Eq, Ord, Show)

gardenPartName :: GardenPart -> String
gardenPartName part = case part of
  NightSky -> "night-sky"
  Moon -> "pierced-moon"
  Ground -> "garden-ground"
  Stems -> "interpolated-stems"
  Leaves -> "profile-leaves"
  Blossoms -> "repeated-profile-blossoms"
  Ribbon -> "flowing-profile-ribbon"
  Fireflies -> "fireflies"

data Palette = Palette !Color !Color !Color

data Plant = Plant
  { plantRoot :: !ExactPoint
  , plantFlower :: !ExactPoint
  , plantScale :: !ExactRational
  , plantTilt :: !ExactRational
  , plantPalette :: !Palette
  }

pearl, blue, lilac, amber :: Palette
pearl = Palette (RGB 40 100 114) (RGB 178 219 207) (RGB 249 246 213)
blue = Palette (RGB 35 68 106) (RGB 115 173 194) (RGB 219 236 227)
lilac = Palette (RGB 68 61 104) (RGB 162 159 194) (RGB 233 219 219)
amber = Palette (RGB 106 85 77) (RGB 211 177 136) (RGB 250 233 181)

plants :: [Plant]
plants =
  [ Plant (exactPoint 305 975) (exactPoint 237 447) (exactHalf + exactThird * exactHalf) (-exactThird) lilac
  , Plant (exactPoint 938 960) (exactPoint 1014 548) (exactHalf + exactHalf ^ (3 :: Int)) exactThird blue
  , Plant (exactPoint 626 971) (exactPoint 596 386) (1 + exactThird * exactHalf) (-exactHalf * exactThird) pearl
  , Plant (exactPoint 429 981) (exactPoint 376 650) (1 - exactHalf ^ (3 :: Int)) exactThird blue
  , Plant (exactPoint 806 973) (exactPoint 849 724) (1 - exactHalf ^ (2 :: Int)) (-exactThird) amber
  ]

-- | One tapered profile is reused for broad petals, narrow leaves and light
-- ribbons. Stations express center/half-span, never Bezier control handles.
lanceolate :: ExactRational -> ExactRational -> ExactRational -> Located ClosedTrail
lanceolate height width bend = profileOutline unitHalf $
  ProfileStation (exactPoint 0 0) (ExactVector 0 0) :|
    [ ProfileStation (exactPoint (bend * exactHalf) (negate height * exactHalf ^ (2 :: Int)))
        (ExactVector (width * exactHalf) 0)
    , ProfileStation (exactPoint bend (negate height * (exactHalf + exactHalf ^ (3 :: Int))))
        (ExactVector width 0)
    , ProfileStation (exactPoint (bend * exactHalf) (negate height * (1 - exactHalf ^ (3 :: Int))))
        (ExactVector (width * exactHalf) 0)
    , ProfileStation (exactPoint 0 (negate height)) (ExactVector 0 0)
    ]

petalOutline :: UnitInterval -> Located ClosedTrail
petalOutline opening =
  lanceolate 142 (19 + 17 * unitIntervalValue opening) (8 * (1 - unitIntervalValue opening))

leafOutline :: UnitInterval -> Located ClosedTrail
leafOutline = leafContour 24

leafContour :: ExactRational -> UnitInterval -> Located ClosedTrail
leafContour width breeze = lanceolate 158 width (24 * (2 * unitIntervalValue breeze - 1))

data StemPort = LowerLeafSocket | UpperLeafSocket | BlossomSocket
  deriving stock (Eq, Ord, Show, Enum, Bounded)

data LeafPort = LeafRoot
  deriving stock (Eq, Ord, Show)

data BlossomPort = BlossomRoot
  deriving stock (Eq, Ord, Show)

-- | An authored socket matrix that collapses, a stem site with no regular
-- tangent frame, or a refused frame policy scalar.
data GardenAttachmentError
  = SingularGardenSocket !ExactPoint !StemPort
  | GardenFrameRefused !ExactPoint !StemPort !FrameError
  | GardenFramePrecisionRefused !RadicalPrecisionError
  | GardenFrameToleranceRefused !ScalarRefinementError
  deriving stock (Eq, Show)

-- An authored heterogeneous assembly, not a generic connection graph. Each
-- child retains its ports until the final ordered painting projection. The
-- stem trail is the one curve the stem paints and its sockets read.
data GardenPlant = GardenPlant
  { gardenStemTrail :: !(Located OpenTrail)
  , gardenStem :: !(Motif StemPort GardenPart)
  , gardenLowerLeaf :: !(Motif LeafPort GardenPart)
  , gardenUpperLeaf :: !(Motif LeafPort GardenPart)
  , gardenBlossom :: !(Motif BlossomPort GardenPart)
  }

gardenPlants :: GardenControls -> Either GardenAttachmentError [GardenPlant]
gardenPlants controls = do
  policy <- stemFramePolicy
  traverse (assemble policy) plants
 where
  leaf :: Motif LeafPort GardenPart
  leaf = leafMotif 24 (gardenBreeze controls)
  assemble :: FramePolicy -> Plant -> Either GardenAttachmentError GardenPlant
  assemble policy plant = do
    let trail = stemTrail controls plant
    stem <- stemMotif policy trail plant
    pure (GardenPlant trail stem
      (attachMotif LeafRoot (motifPort stem LowerLeafSocket) leaf)
      (attachMotif LeafRoot (motifPort stem UpperLeafSocket) leaf)
      (attachMotif BlossomRoot (motifPort stem BlossomSocket)
        (blossomMotif controls (plantPalette plant))))

gardenPicture :: GardenControls -> Either GardenAttachmentError (Picture GardenPart)
gardenPicture controls = do
  assemblies <- gardenPlants controls
  pure $ annotate NightSky sky
    <> annotate Moon moon
    <> annotate Ground ground
    <> annotate Stems (foldMap (motifPicture . gardenStem) assemblies <> grass)
    <> annotate Leaves (foldMap (\plant -> motifPicture (gardenLowerLeaf plant)
      <> motifPicture (gardenUpperLeaf plant)) assemblies)
    <> annotate Blossoms (foldMap (motifPicture . gardenBlossom) assemblies)
    <> annotate Ribbon ribbon
    <> annotate Fireflies fireflies

-- | Breeze bends the stem by moving its interpolated knots. This authors the
-- knots only; sockets read the stem trail itself, never this formula.
stemStation :: GardenControls -> Plant -> ExactRational -> ExactPoint
stemStation controls plant t =
  let (rx, ry) = exactPointCoordinates (plantRoot plant)
      (fx, fy) = exactPointCoordinates (plantFlower plant)
      sway = 86 * (2 * unitIntervalValue (gardenBreeze controls) - 1)
   in exactPoint (rx + t * (fx - rx) + 4 * t * (1 - t) * sway) (ry + t * (fy - ry))

-- | The root, two leaf knots and the flower, interpolated once. Knot @k@ is
-- the start of step @k@; the flower is the trail's end.
stemTrail :: GardenControls -> Plant -> Located OpenTrail
stemTrail controls plant = cardinalOpen unitHalf $ Interpolating (plantRoot plant) :|
  (Interpolating . stemStation controls plant <$> [exactThird, 2 * exactThird, 1])

-- | Leaf frames are normalized to within 2^-20 of unit scale, so the authored
-- leaf size survives the tangent frame.
stemFramePolicy :: Either GardenAttachmentError FramePolicy
stemFramePolicy = framePolicy
  <$> first GardenFramePrecisionRefused (radicalPrecision 32)
  <*> first GardenFrameToleranceRefused (positiveExact (exactHalf ^ (20 :: Int)))

plantStem :: Located OpenTrail -> Picture GardenPart
plantStem stem = ink positiveTwo (RGB 26 73 72) stem <> ink positiveOne (RGB 100 148 116) stem

-- | Leaf sockets keep their authored knots and take the stem's tangent frame
-- there; side and size are an authored lean relative to that tangent. The
-- blossom socket keeps its authored tilt at the flower, the trail's end.
stemMotif
  :: FramePolicy -> Located OpenTrail -> Plant
  -> Either GardenAttachmentError (Motif StemPort GardenPart)
stemMotif policy stem plant = do
  lower <- leafSocket LowerLeafSocket 1 (-1) 1
  upper <- leafSocket UpperLeafSocket 2 1 (1 - exactThird)
  flower <- admit BlossomSocket $ affine2
    (ExactVector (plantScale plant) (plantTilt plant * plantScale plant * exactHalf))
    (ExactVector 0 (plantScale plant * (1 - exactHalf ^ (3 :: Int))))
    (pointVector (plantFlower plant))
  pure $ motif (plantStem stem) $ \port -> case port of
    LowerLeafSocket -> lower
    UpperLeafSocket -> upper
    BlossomSocket -> flower
 where
  admit :: StemPort -> Affine2 -> Either GardenAttachmentError AffineIso2
  admit port = maybe (Left (SingularGardenSocket (plantRoot plant) port)) Right . affineIso2
  -- The quarter-turn taking the leaf's forward axis, local -y, onto the
  -- frame's tangent, followed by the lean: on an upright stem this is the
  -- former world-axis socket matrix.
  leafSocket :: StemPort -> Int -> ExactRational -> ExactRational -> Either GardenAttachmentError AffineIso2
  leafSocket port knot side size = do
    let scale = size * (exactHalf + plantScale plant * exactHalf)
        refused = GardenFrameRefused (plantRoot plant) port
    lean <- admit port $ affine2
      (ExactVector (negate side * scale) (scale * exactHalf))
      (ExactVector (negate scale * exactHalf) (negate side * scale))
      (ExactVector 0 0)
    site <- first refused (exactTrailSite (OpenSubpath stem) knot unitZero)
    frame <- first refused (regularFrame policy (ExactSite site))
    pure (composeAffineIso2 (measuredFrameIso frame) lean)

leafMotif :: ExactRational -> UnitInterval -> Motif LeafPort GardenPart
leafMotif width breeze = motif picture (const identityAffineIso2)
 where
  outline :: Located ClosedTrail
  outline = leafContour width breeze
  vein :: Located OpenTrail
  vein = bowedTrail ((2 * unitIntervalValue breeze - 1) * exactHalf ^ (3 :: Int))
    (exactPoint 0 0) (exactPoint 0 (-158))
  picture :: Picture GardenPart
  picture = fill NonZero (gradient (exactPoint (negate width) 0) (exactPoint width (-138))
            (Palette (RGB 13 44 52) (RGB 48 104 96) (RGB 114 162 130))) (outline :| [])
          <> clip NonZero (outline :| [])
            (ink positiveOne (RGB 151 180 133) vein
              <> foldMap sideVein [1 .. 5 :: Int])
  sideVein :: Int -> Picture GardenPart
  sideVein index =
    let y = negate (22 + 20 * fromIntegral index)
     in opacity unitHalf $ foldMap
          (\side -> ink positiveOne (RGB 96 150 120)
            (bowedTrail (side * exactHalf ^ (3 :: Int)) (exactPoint 0 (y + 14)) (exactPoint (side * (width + 2)) y)))
          [-1, 1]

blossomMotif :: GardenControls -> Palette -> Motif BlossomPort GardenPart
blossomMotif controls palette = motif picture (const identityAffineIso2)
 where
  picture :: Picture GardenPart
  picture = opacity unitHalf (place (uniformAt 170 (exactPoint 0 8))
    (fill NonZero (RadialGradient (exactPoint 0 0) positiveOne
      (gradientStops (GradientStop unitZero (RGB 131 199 168) unitHalf :|
        [GradientStop unitOne (RGB 57 130 136) unitZero])))
      (circle positiveOne :| [])))
    <> petals (blossomOpening controls) palette

-- | The same leaves, with geometric bounds and real port axes. Widening the
-- middle specimen displaces its successor; there is no authored successor x.
gardenLayoutPicture :: UnitInterval -> Picture GardenPart
gardenLayoutPicture opening =
  sky <> annotate Leaves (foldMap (\value -> motifPicture value <> diagnostic value) specimens)
 where
  specimens :: Seq.Seq (Motif LeafPort GardenPart)
  specimens = arrangeMotifs Horizontal 75 $ fmap
    (alignMotif Vertical unitOne 640 . transformMotif (translationAffineIso2 (ExactVector 150 0))) $
    Seq.fromList [leafMotif 24 unitHalf, leafMotif (24 + 70 * unitIntervalValue opening) unitHalf,
      leafMotif 24 unitHalf]
  diagnostic :: Motif LeafPort GardenPart -> Picture GardenPart
  diagnostic value = portAxes (motifPort value LeafRoot) <>
    case geometryBounds (geometryEnvelope (motifPicture value)) of
      Nothing -> mempty
      Just bounds -> stroke (StrokeStyle (Solid (RGB 182 164 109)) positiveOne
          OutputUnits ButtCap BevelJoin) $ path $ Seq.singleton $ ClosedSubpath $
        polygonTrail (exactPoint (boundsMinimumX bounds) (boundsMinimumY bounds) :|
          [ exactPoint (boundsMaximumX bounds) (boundsMinimumY bounds)
          , exactPoint (boundsMaximumX bounds) (boundsMaximumY bounds)
          , exactPoint (boundsMinimumX bounds) (boundsMaximumY bounds) ])

portAxes :: AffineIso2 -> Picture GardenPart
portAxes frame = foldMap axis [ExactVector 16 0, ExactVector 0 (-16)]
 where
  axis :: ExactVector -> Picture GardenPart
  axis direction = place (affineIsoMap frame) $
    ink positiveTwo (RGB 239 157 120)
      (locate (exactPoint 0 0) (openTrail (Seq.singleton (curveStep line direction))))

-- Exact rational frames: cardinal quarter-turns and scaled diagonal rotations.
-- One petal value supplies every spoke and both differently sized whorls.
spokes :: [(ExactRational, ExactRational)]
spokes =
  [(0,-1),(diagonal,-diagonal),(1,0),(diagonal,diagonal)
  ,(0,1),(-diagonal,diagonal),(-1,0),(-diagonal,-diagonal)]
 where
  diagonal :: ExactRational
  diagonal = 3 * exactHalf ^ (2 :: Int)

petals :: UnitInterval -> Palette -> Picture GardenPart
petals opening palette =
  whorl
    <> place (affine2 (ExactVector exactHalf exactHalf) (ExactVector (-exactHalf) exactHalf) (ExactVector 0 0))
      whorl
    <> flowerHeart
 where
  whorl :: Picture GardenPart
  whorl = foldMap (\direction -> place (spokeFrame direction) petal) spokes
  outline :: Located ClosedTrail
  outline = petalOutline opening
  petal :: Picture GardenPart
  petal = fill NonZero (gradient (exactPoint 0 6) (exactPoint 0 (-145)) palette) (outline :| [])
    <> clip NonZero (outline :| [])
      (opacity unitHalf
        (fill NonZero (Solid (RGB 238 243 216)) (lanceolate 138 3 4 :| []))
        <> ink positiveOne (RGB 137 184 170)
          (bowedTrail (exactHalf ^ (5 :: Int)) (exactPoint 0 0) (exactPoint 0 (-136))))
    <> stroke (StrokeStyle (Solid (RGB 172 207 189)) positiveOne OutputUnits RoundCap RoundJoin)
      (path (Seq.singleton (ClosedSubpath outline)))

flowerHeart :: Picture GardenPart
flowerHeart =
  fill NonZero (gradient (exactPoint (-20) (-20)) (exactPoint 20 20)
    (Palette (RGB 234 209 141) (RGB 159 158 87) (RGB 69 108 92)))
    (ellipse (ExactVector 25 0) (ExactVector 0 21) :| [])
    <> foldMap pollen spokes
    <> fill NonZero (Solid (RGB 230 223 161)) (circle positiveTwo :| [])
 where
  pollen :: (ExactRational, ExactRational) -> Picture GardenPart
  pollen (x, y) = place (uniformAt 2 (exactPoint (15*x) (12*y))) $
    fill NonZero (Solid (RGB 245 225 153)) (circle positiveOne :| [])

sky :: Picture GardenPart
sky = fill NonZero (gradient (exactPoint 150 0) (exactPoint 1050 1050)
    (Palette (RGB 5 18 31) (RGB 19 46 58) (RGB 7 22 34)))
    (polygonTrail (exactPoint 0 0 :| [exactPoint 1200 0, exactPoint 1200 1000, exactPoint 0 1000]) :| [])
  <> place (affine2 (ExactVector 550 0) (ExactVector 0 410) (ExactVector 830 310))
    (fill NonZero (RadialGradient (exactPoint 0 0) positiveOne
      (gradientStops (GradientStop unitZero (RGB 61 99 103) unitHalf :|
        [GradientStop unitOne (RGB 23 58 75) unitZero]))) (circle positiveOne :| []))
  <> foldMap star [0 .. 71 :: Int]
 where
  star :: Int -> Picture GardenPart
  star index =
    let x = 48 + fromIntegral ((index * 173 + 41) `mod` 1100)
        y = 44 + fromIntegral ((index * 97 + 17) `mod` 640)
        size = exactHalf + fromIntegral (index `mod` 3) * exactHalf
     in opacity unitHalf $ place (uniformAt size (exactPoint x y)) $
          fill NonZero (Solid (RGB 182 207 189)) (spark :| [])

moon :: Picture GardenPart
moon = place (uniformAt 152 (exactPoint 943 219)) $
  let outer = circle positiveOne
      hole = transformLocatedClosedTrail
        (uniformAt 1 (exactPoint exactHalf (-exactThird))) outer
      -- Clip the overlapping even-odd circles to the outer disc: the cutout
      -- is transparent and cannot create a second lobe outside the moon.
      moonDisc :: Picture GardenPart
      moonDisc = fill EvenOdd (gradient (exactPoint (-1) (-1)) (exactPoint 1 1)
        (Palette (RGB 225 231 195) (RGB 150 183 168) (RGB 74 123 128))) (outer :| [hole])
   in clip NonZero (outer :| []) moonDisc

ground :: Picture GardenPart
ground = place (uniformAt 1 (exactPoint 614 962)) $
  fill NonZero (gradient (exactPoint 0 (-75)) (exactPoint 0 38)
    (Palette (RGB 27 64 65) (RGB 13 39 48) (RGB 5 20 31)))
    (ellipse (ExactVector 598 0) (ExactVector 0 83) :| [])

grass :: Picture GardenPart
grass = opacity unitHalf $ foldMap blade [0 .. 17 :: Int]
 where
  blade :: Int -> Picture GardenPart
  blade index =
    let x = 150 + fromIntegral (index * 53)
        height = 90 + fromIntegral ((index * 71) `mod` 135)
        lean = fromIntegral ((index * 31) `mod` 90) - 45
     in ink positiveOne (RGB 76 124 111)
          (bowedTrail (lean * exactHalf ^ (8 :: Int)) (exactPoint x 976) (exactPoint (x+lean) (976-height)))

ribbon :: Picture GardenPart
ribbon =
  let stations = ProfileStation (exactPoint 205 904) (ExactVector 0 0) :|
        [ProfileStation (exactPoint 380 925) (ExactVector (-4) 15)
        ,ProfileStation (exactPoint 605 871) (ExactVector 0 21)
        ,ProfileStation (exactPoint 829 895) (ExactVector 3 16)
        ,ProfileStation (exactPoint 1023 852) (ExactVector 0 0)]
      outline = profileOutline unitHalf stations
   in fill NonZero (gradient (exactPoint 230 940) (exactPoint 1000 835)
        (Palette (RGB 61 88 87) (RGB 147 168 136) (RGB 51 102 99))) (outline :| [])
      <> clip NonZero (outline :| [])
        (ink positiveOne (RGB 211 211 158)
          (cardinalOpen unitHalf (Interpolating (exactPoint 205 904) :|
            (Interpolating <$> [exactPoint 380 930,exactPoint 605 880,exactPoint 829 899,exactPoint 1023 852]))))

fireflies :: Picture GardenPart
fireflies = foldMap glow [(142,685),(525,195),(735,576),(1101,753),(618,754),(279,816)]
 where
  glow :: (ExactRational, ExactRational) -> Picture GardenPart
  glow (x,y) = place (uniformAt 1 (exactPoint x y)) $
    place (uniformAt 15 (exactPoint 0 0))
      (fill NonZero (RadialGradient (exactPoint 0 0) positiveOne
        (gradientStops (GradientStop unitZero (RGB 231 224 143) unitHalf :|
          [GradientStop unitOne (RGB 163 198 157) unitZero]))) (circle positiveOne :| []))
      <> fill NonZero (Solid (RGB 250 239 174)) (spark :| [])

spark :: Located ClosedTrail
spark = polygonTrail (exactPoint 0 (-3) :|
  [exactPoint exactHalf (-exactHalf),exactPoint 3 0,exactPoint exactHalf exactHalf
  ,exactPoint 0 3,exactPoint (-exactHalf) exactHalf,exactPoint (-3) 0,exactPoint (-exactHalf) (-exactHalf)])

-- These local helpers are shared painting/placement expressions within this
-- artwork, not another public rendering or geometry layer.
gradient :: ExactPoint -> ExactPoint -> Palette -> Paint
gradient a b (Palette dark middle light) = LinearGradient a b $
  gradientStops (GradientStop unitZero dark unitOne :|
    [GradientStop unitHalf middle unitOne, GradientStop unitOne light unitOne])

ink :: PositiveExact -> Color -> Located OpenTrail -> Picture GardenPart
ink width color curve = stroke (StrokeStyle (Solid color) width OutputUnits RoundCap RoundJoin)
  (path (Seq.singleton (OpenSubpath curve)))

uniformAt :: ExactRational -> ExactPoint -> Affine2
uniformAt scale center = affine2 (ExactVector scale 0) (ExactVector 0 scale) (pointVector center)

spokeFrame :: (ExactRational, ExactRational) -> Affine2
spokeFrame (x,y) = affine2 (ExactVector (negate y) x) (ExactVector (negate x) (negate y)) (ExactVector 0 0)

pointVector :: ExactPoint -> ExactVector
pointVector = uncurry ExactVector . exactPointCoordinates