{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE OverloadedStrings #-} -------------------------------------------------------------------------------- -- | -- Module : Data.Geometry.Svg.MathCoordinateSystem -- Copyright : (C) Frank Staals -- License : see the LICENSE file -- Maintainer : Frank Staals -- Description : -- -- Defines functions to make sure we render the coordinate system in -- svg correctly, i.e. with the origin in the bottom-left instead of -- top-left. -- -------------------------------------------------------------------------------- module Data.Geometry.Svg.MathCoordinateSystem( Canvas(Canvas) , center, dimensions, zoomLevel , createCanvas , renderCanvas, text_ , realWorldCoordinates , toAValue, toPValue, showP ) where import Control.Lens hiding (view, element) import Data.Fixed import Data.Geometry.Point import Data.Geometry.Vector import qualified Data.List as List import Data.Text (Text) import Data.Util (SP(..)) import Prelude hiding ((!!)) import Text.Blaze.Internal (Attributable(..)) import Text.Blaze.Svg11 ((!)) import qualified Text.Blaze.Svg11 as Svg import qualified Text.Blaze.Svg11.Attributes as A -------------------------------------------------------------------------------- -- | Svg Canvas that has a "proper" Coordinate system whose origin is in the bottom left. data Canvas r = Canvas { _dimensions :: Vector 2 Int -- ^ dimensions (width,height) of the canvas , _center :: Point 2 r -- ^ the center point (in world coordinates) -- of the viewport of the canvas. , _zoomLevel :: r -- ^ determines the zoomlevel of the -- canvas. At zoomlevel z the width and -- height (in terms of world coordinates) -- that we can see are z*dimensions } deriving (Show,Eq) center :: Lens' (Canvas r) (Point 2 r) center = lens _center (\cv c -> cv { _center = c } ) dimensions :: Lens' (Canvas r) (Vector 2 Int) dimensions = lens _dimensions (\cv c -> cv { _dimensions = c } ) zoomLevel :: Lens' (Canvas r) r zoomLevel = lens _zoomLevel (\cv c -> cv { _zoomLevel = c } ) -------------------------------------------------------------------------------- -- | Create a canvas createCanvas :: Num r => Int -> Int -> Canvas r createCanvas w h = Canvas (Vector2 w h) (fromIntegral <$> Point2 (w `div` 2) (h `div` 2)) 1 -------------------------------------------------------------------------------- -- | Draws the actual canvas renderCanvas :: RealFrac r => Canvas r -> [Svg.Attribute] -> Svg.Svg -> Svg.Svg renderCanvas cv ats vs = Svg.svg ! A.width (toPValue w) ! A.height (toPValue h) ! A.viewbox outerVB ! A.style "border-style: solid" !! ats $ Svg.g ! A.transform "scale(1,-1)" $ Svg.svg ! A.width "100%" ! A.height "100%" ! A.viewbox innerVB $ vs where Vector2 w h = cv^.dimensions SP (Point2 lx ly) (Vector2 vw vh) = bimap (fmap round) (fmap round) $ viewRectangle cv toVB = mconcat @Svg.AttributeValue . List.intersperse " " . map toPValue outerVB = toVB [0, (-1) * h, w, h] -- the role of the outer viewBox is to flip the coordinate -- system s.t. the origin is in the bottom left rather -- than the top-left innerVB = toVB [lx, ly, vw, vh] -- | Computes the view rectangle of the canvas; given by the lower left point and -- the dimensions (in real coordinates). viewRectangle :: Fractional r => Canvas r -> SP (Point 2 r) (Vector 2 r) viewRectangle cv = SP (Point2 (cx - (vw / 2)) (cy - (vh / 2))) dims where Point2 cx cy = cv^.center dims@(Vector2 vw vh) = (1 / cv^.zoomLevel) *^ (fromIntegral <$> cv^.dimensions) infixl 9 !! (!!) :: Attributable t => t -> [Svg.Attribute] -> t t !! ats = List.foldl' (!) t ats -- | To be used instead of the text_ combinator in Blaze text_ :: Real r => Point 2 r -- ^ position where to draw (in world coordinates) -> [Svg.Attribute] -> Text -> Svg.Svg text_ (Point2 x y) ats t = Svg.g ! A.transform (mconcat [ "translate(" , toPValue x , ", " , toPValue y , ")scale(1,-1)" ]) $ Svg.text_ !! ats $ Svg.text t -------------------------------------------------------------------------------- -- | Computes the mouse position in terms of real world coordinates. -- pre: the coordinates given lie on the canvas realWorldCoordinates :: Fractional r => Canvas r -> Point 2 Int -> Point 2 r realWorldCoordinates cv (Point2 x y) = applyViewBox cv $ Point2 x ((cv^.dimensions.element (C @ 1)) - y) -- position relative to the outer viewbox -- | Applies the viewbox transformation applyViewBox :: Fractional r => Canvas r -> Point 2 Int -> Point 2 r applyViewBox cv p = Point2 (lx + (x/w) * vw) (ly + (y/h)*vh) where (Vector2 w h) = fromIntegral <$> cv^.dimensions SP (Point2 lx ly) (Vector2 vw vh) = viewRectangle cv Point2 x y = fromIntegral <$> p -------------------------------------------------------------------------------- toAValue :: Show a => a -> Svg.AttributeValue toAValue = Svg.toValue . show toPValue :: Real r => r -> Svg.AttributeValue toPValue = toAValue . showP -- | show by converting to a Pico showP :: Real a => a -> Pico showP = realToFrac