packages feed

gloss-relative-0.1.2.0: src/Graphics/Gloss/Relative/Internal/Picture.hs

module Graphics.Gloss.Relative.Internal.Picture where

import Graphics.Gloss
import Graphics.Gloss.Relative.Internal.Dimension

import Data.Semigroup
import Data.Monoid

-- * Pictures

translateX :: Float -> Picture -> Picture
translateX x p = Translate x 0 p

translateY :: Float -> Picture -> Picture
translateY y p = Translate 0 y p

-- The default Monoid instance for 'Picture' is buggy, since Blank is not in fact a neutral element.
emptyPicture :: Picture
emptyPicture = Pictures []

joinPicture :: Picture -> Picture -> Picture
joinPicture (Pictures xs) (Pictures ys) = catPictures $ xs ++ ys
joinPicture x (Pictures ys) = catPictures (x : ys)
joinPicture (Pictures xs) y = catPictures (xs ++ [y])
joinPicture x y = catPictures [x,y]

catPictures :: [Picture] -> Picture
catPictures [x] = x
catPictures xs = Pictures xs

-- | Flattens a picture, by eliminating some nested transforms.
-- NOTE: OpenGL backends have a nested transforms stack of only 32, so we need to make sure to flatten Pictures before rendering to avoid stack overflows.
flattenPicture :: Picture -> Picture
flattenPicture p = Pictures $ flatten identityMatrix p

-- Representing Affine Transformation Matrices.
-- | a c e |
-- | b d f |
-- | 0 0 1 |
data Matrix = Matrix Float Float Float Float Float Float

identityMatrix :: Matrix
identityMatrix = Matrix 1 0 0 1 0 0

isShearedMatrix :: Matrix -> Bool
isShearedMatrix (Matrix a b c d _ _) = 
    let dotProduct = a * c + b * d
        epsilon = 0.0001
    in abs dotProduct > epsilon

multiplyMatrix :: Matrix -> Matrix -> Matrix
multiplyMatrix (Matrix a1 b1 c1 d1 e1 f1) (Matrix a2 b2 c2 d2 e2 f2) =
    Matrix (a1*a2 + c1*b2) (b1*a2 + d1*b2)
           (a1*c2 + c1*d2) (b1*c2 + d1*d2)
           (a1*e2 + c1*f2 + e1) (b1*e2 + d1*f2 + f1)

flatten :: Matrix -> Picture -> [Picture]
flatten m pic | isShearedMatrix m = [transformLeaf m pic]
flatten m Blank = []
flatten m (Pictures ps) = concatMap (flatten m) ps
flatten m (Translate x y p) = flatten (m `multiplyMatrix` Matrix 1 0 0 1 x y) p
flatten m (Scale sx sy p) = flatten (m `multiplyMatrix` Matrix sx 0 0 sy 0 0) p
flatten m (Rotate deg p) =
        let rad = -deg * pi / 180 -- Gloss is clockwise
            s = sin rad
            c = cos rad
        in flatten (m `multiplyMatrix` Matrix c (-s) s c 0 0) p
flatten m (Polygon points) = [Polygon (map (transformPoint m) points)]
flatten m (Line points) = [Line (map (transformPoint m) points)]
flatten m pic = [transformLeaf m pic]

transformPoint :: Matrix -> Point -> Point
transformPoint (Matrix a b c d e f) (x, y) = 
    (a*x + c*y + e, b*x + d*y + f)

transformLeaf :: Matrix -> Picture -> Picture
transformLeaf (Matrix a b c d e f) leaf =
    let
        tx = e
        ty = f
        sx = sqrt (a*a + b*b)
        -- Use the determinant to handle negative scaling/flipping
        det = a*d - b*c
        sy = (signum det) * sqrt (c*c + d*d)
        angle = atan2 b a * 180 / pi
    in
        Translate tx ty $ Rotate (-angle) $ Scale sx sy leaf

-- * Regions

-- | A rectangular region within the screen.
data Region = Region
    { regionLeftTop :: Point
    , regionDimension :: Dimension
    } deriving (Eq,Ord,Show)

instance Semigroup Region where
    (Region (x1,y1) (w1,h1)) <> (Region (x2,y2) (w2,h2)) = Region (xmin,ymin) (abs (xmax-xmin),abs (ymax-ymin))
         where xmin = min x1 x2
               ymin = min y1 y2
               xmax = max (x1+w1) (x2+w2)
               ymax = max (y1+h1) (y2+h2)

instance Monoid Region where
    mempty = Region (0,0) (0,0)

regionScreenSize :: Region -> ScreenSize
regionScreenSize (Region (x,y) (xdim,ydim)) = (ceiling $ maxX * 2,ceiling $ maxY * 2)
    where
    maxX = max (abs x) (abs $ x + xdim)
    maxY = max (abs y) (abs $ y + ydim)

dimensionToRegion :: Dimension -> Region
dimensionToRegion (w,h) = Region { regionLeftTop = (-w/2,h/2), regionDimension = (w,h) }

pointInsideRegion :: Point -> Region -> Bool
pointInsideRegion (mouseX,mouseY) r =
    let (regionLeft,regionTop) = regionLeftTop r
        (w,h) = regionDimension r 
        regionRight = regionLeft + w 
        regionBottom = regionTop - h 
    in (regionLeft <= mouseX && mouseX <= regionRight) && (regionBottom <= mouseY && mouseY <= regionTop)

-- converts a point inside a region to a relative point within the region
pointWithinRegion :: Point -> Region -> Maybe Point
pointWithinRegion p@(x,y) r = if pointInsideRegion p r
    then
        let (regionLeft,regionTop) = regionLeftTop r 
            (w,h) = regionDimension r
            regionCenterX = regionLeft + w / 2
            regionCenterY = regionTop - h / 2
        in Just (x - regionCenterX,y - regionCenterY)
    else Nothing

translateRegionX :: Float -> Region -> Region
translateRegionX f r = r { regionLeftTop = translatePointX f (regionLeftTop r) }

translateRegionY :: Float -> Region -> Region
translateRegionY f r = r { regionLeftTop = translatePointY f (regionLeftTop r) }

-- * Picture bounding box

-- | Estimates the 'Region' of a 'Picture'.
pictureRegion :: Picture -> Region
pictureRegion = bbToRegion . bbox

pictureScreenSize :: Picture -> ScreenSize
pictureScreenSize = regionScreenSize . pictureRegion 

pictureDimension :: Picture -> Dimension
pictureDimension = screenSizeToDimension . pictureScreenSize

type Elipsis = (Float,Float,Float,Float,Float,Float)
data BB = BB { bbElipsis :: [Elipsis], bbPoints :: [Point] } deriving Show

instance Semigroup BB where
    (BB es1 ps1) <> (BB es2 ps2) = BB (es1++es2) (ps1++ps2)
instance Monoid BB where
    mempty = BB [] []

bbox :: Picture -> BB
bbox Blank = mempty
bbox (Polygon l) = BB [] l
bbox (Line l) = BB [] l
bbox (Circle r) = case (circElipsis r) of
    Nothing -> mempty
    Just e -> BB [e] []
bbox (ThickCircle r thick) = case (circElipsis $ r + thick / 2) of
    Nothing -> mempty
    Just e -> BB [e] []
bbox (Arc _ _ r) = bbox (Circle r) -- big overapproximation
bbox (ThickArc _ _ r thick) = bbox (ThickCircle r thick) -- big overapproximation
bbox (Text str) = bbox $ Translate (textWidth/2) (fromIntegral charHeight/2) rect
    where
    rect = rectangleWire textWidth (realToFrac charHeight)
    textWidth = realToFrac (length str) * (realToFrac charWidth)
bbox (Bitmap bmp) = bbox $ rectangleWire (fromIntegral w) (fromIntegral h)
    where (w,h) = bitmapSize bmp
bbox (BitmapSection rect bmp) = bbox $ rectangleWire (fromIntegral w) (fromIntegral h)
    where (w,h) = rectSize rect
bbox (Color c p) = bbox p
bbox (Scale x y p) = scaleBB x y (bbox p)
bbox (Translate x y p) = translateBB x y (bbox p)
bbox (Rotate alpha p) = rotateBB alpha (bbox p)
bbox (Pictures xs) = mconcat $ map bbox xs

charHeight :: Int
charHeight =  104
charWidth :: Int
charWidth = 70

scaleBB :: Float -> Float -> BB -> BB
scaleBB x y (BB es ps) = BB (map (scaleElipsis x y) es) (scalePath x y ps)

translateBB :: Float -> Float -> BB -> BB
translateBB x y (BB es ps) = BB (map (translateElipsis x y) es) (translatePath x y ps)

rotateBB :: Float -> BB -> BB
rotateBB alpha (BB es ps) = BB (map (rotateElipsis alpha) es) (rotatePath alpha ps)

bbToRegion :: BB -> Region
bbToRegion (BB es ps) = mconcat $ pathRegion ps : map elipsisRegion es

pathRegion :: Path -> Region
pathRegion [] = mempty
pathRegion l = Region (xmin,ymin) (abs (xmax-xmin),abs (ymax-ymin))
 where (xs,ys) = unzip l
       xmin = minimum xs
       ymin = minimum ys
       xmax = maximum xs
       ymax = maximum ys

elipsisRegion :: Elipsis -> Region
elipsisRegion (a,b,c,d,e,f) | dlt==0 = mempty
                        | otherwise = Region (xl,yb) (abs (xr-xl),abs (yt-yb))
 where dlt = 4*a*c - b^2
       xc = (b*e - 2*c*d) / dlt
       yc = (b*d - 2*a*e) / dlt
       xr = xc + sqrt ((2*b*e-4*c*d)^2+4*dlt*(e^2-4*c*f)) / (2*dlt)
       yt = yc + sqrt ((2*b*d-4*a*e)^2+4*dlt*(d^2-4*a*f)) / (2*dlt)
       xl = xc - sqrt ((2*b*e-4*c*d)^2+4*dlt*(e^2-4*c*f)) / (2*dlt)
       yb = yc - sqrt ((2*b*d-4*a*e)^2+4*dlt*(d^2-4*a*f)) / (2*dlt)

-- converts point from cartesian to polar coordinates
toPolar :: Point -> (Float,Float)
toPolar (x,y) = (sqrt (x^2+y^2), atan2 y x)

-- converts point from polar to cartesian
toCartesian :: (Float,Float) -> Point
toCartesian (r,gama) = (r*cos gama,r*sin gama)

-- a circle as an elipsis
circElipsis :: Float -> Maybe Elipsis
circElipsis 0 = Nothing
circElipsis r = Just (1/r^2,0,1/r^2,0,0,-1)

rotatePoint :: Float -> Point -> Point
rotatePoint alpha (x,y) = toCartesian (r,gama + (rad (-alpha)))
 where (r,gama) = toPolar (x,y)
 
rotatePath :: Float -> Path -> Path
rotatePath alpha = map (rotatePoint alpha)

rotateElipsis :: Float -> Elipsis -> Elipsis
rotateElipsis alpha (a,b,c,d,e,f) = (a',b',c',d',e',f')
 where theta = rad (alpha)
       a' = a*(cos theta)^2 + b*sin theta*cos theta + c*(sin theta)^2
       b' = 2*(c-a)*sin theta*cos theta + b*((cos theta)^2-(sin theta)^2)
       c' = a*(sin theta)^2 - b*sin theta*cos theta + c*(cos theta)^2
       d' = d*cos theta + e*sin theta
       e' = -d*sin theta + e*cos theta
       f' = f

translatePoint :: Float -> Float -> Point -> Point
translatePoint x y (a,b) = (a+x,b+y)

translatePointX :: Float -> Point -> Point
translatePointX f (x,y) = (x+f,y)

translatePointY :: Float -> Point -> Point
translatePointY f (x,y) = (x,y+f)

translatePath :: Float -> Float -> Path -> Path
translatePath x y = map (translatePoint x y)

translateElipsis :: Float -> Float -> Elipsis -> Elipsis
translateElipsis x y (a,b,c,d,e,f) = (a',b',c',d',e',f')
 where a' = a
       b' = b
       c' = c
       d' = d - 2*a*x - b*y
       e' = e - b*x - 2*c*y
       f' = f + a*x^2 + b*x*y + c*y^2 - d*x -e*y

scalePoint :: Float -> Float -> Point -> Point
scalePoint xs ys (x,y) = (x*xs,y*ys)

scalePath :: Float -> Float -> Path -> Path
scalePath xs ys = map (scalePoint xs ys)

scaleElipsis :: Float -> Float -> Elipsis -> Elipsis
scaleElipsis xs ys (a,b,c,d,e,f) = (a',b',c',d',e',f')
 where a' = a / (xs^2)
       b' = b / (xs * ys)
       c' = c / (ys^2)
       d' = d / xs
       e' = e / ys
       f' = f

mulPointwise :: Point -> Point -> Point
mulPointwise (a,b) (c,d) = (a*c,b*d)

-- * Auxiliary functions

-- | Converts degrees to radians
rad :: Float -> Float
rad alpha = alpha * pi / 180

-- | Converts radians to degrees
deg :: Float -> Float
deg alpha = alpha * 180 / pi