packages feed

L-seed-0.2: src/Lseed/Renderer/Cairo.hs

module Lseed.Renderer.Cairo where

import Graphics.UI.Gtk hiding (fill)
import Graphics.Rendering.Cairo
import Control.Monad
import Control.Concurrent
import Data.IORef
import Data.Maybe
import Lseed.Data
import Lseed.Data.Functions
import Lseed.Constants
import Lseed.Geometry
import Lseed.StipeInfo
import Text.Printf
import System.Time
import qualified Data.Map as M
import Data.List
import Data.Ord
import System.Time

colors :: [ (Double, Double, Double) ]
colors = cycle $ [ (r,g,b) | r <- [0.0,0.4], b <- [0.0, 0.4], g <- [1.0,0.6,0.8]]

pngDailyObserver :: FilePath -> Observer
pngDailyObserver filename = nullObserver {
	obGrowingState = \scGen -> do
		ScreenContent garden angle timeInfo mbMessage <-
			scGen `fmap` getClockTime 
		let (w,h) = (800,600)
		let h' = fromIntegral h / fromIntegral w

		let (xLeft,xRight,xHeight) = gardenOffset garden
		    scaleY = h'*(1-groundLevel)/xHeight
		    scaleX = 1/(max 1 xRight - min 0 xLeft)
		    scaleXY = minimum [1, scaleX, scaleY]
		    shiftX = min 0 xLeft 

		withImageSurface FormatRGB24 w h $ \sur -> do
			renderWith sur $ do
				-- Set up coordinates
				translate 0 (fromIntegral h)
				scale 1 (-1)
				scale (fromIntegral w) (fromIntegral w)
				translate 0 groundLevel
				setLineWidth stipeWidth

				preserve $ do
					scale scaleXY scaleXY
					translate (-shiftX) 0
					render angle garden

				maybe (return ()) (renderMessage angle h') mbMessage
				renderTimeInfo timeInfo
				renderStats h' garden
			surfaceWriteToPNG sur filename
	}

pngObserver :: IO Observer
pngObserver = return $ nullObserver {
	obFinished = \garden -> do
		let (w,h) = (400,400)
		withImageSurface FormatRGB24 w h $ \sur -> do
			renderWith sur $ do
				-- Set up coordinates
				translate 0 (fromIntegral h)
				scale 1 (-1)
				scale (fromIntegral w) (fromIntegral w)
				translate (-0.5) 0
				scale 2 2
				translate 0 groundLevel
				setLineWidth stipeWidth

				render (pi/3) (annotateGarden (pi/3) garden)
			surfaceWriteToPNG sur "/dev/fd/1"
	}

cairoObserver :: IO Observer
cairoObserver = do
	initGUI

	-- global renderer state
	currentGardenRef <- newIORef (const (ScreenContent [] (pi/2) "No time yet" Nothing))

	-- widgets
	canvas <- drawingAreaNew

	window <- windowNew
	set window [windowDefaultWidth := 800, windowDefaultHeight := 600,
	      containerChild := canvas, containerBorderWidth := 0]
	widgetShowAll window

	-- Make gtk and haskell threading compatible
	timeoutAdd (yield >> return True) 50
	
	-- a thread for our GUI
	forkIO $ mainGUI

	-- The actual drawing function
	onExpose canvas$ \e -> do scGen <- readIORef currentGardenRef
				  ScreenContent garden angle timeInfo mbMessage <-
						scGen `fmap` getClockTime 
				  s <- clockTimeToDouble `fmap` getClockTime
				  dwin <- widgetGetDrawWindow canvas
				  (w,h) <- drawableGetSize dwin
				  let h' = fromIntegral h / fromIntegral w

				  let (xLeft,xRight,xHeight) = gardenOffset garden
				      scaleY = h'*(1-groundLevel)/xHeight
				      scaleX = 1/(max 1 xRight - min 0 xLeft)
				      scaleXY = minimum [1, scaleX, scaleY]
				      shiftX = min 0 xLeft 

				  renderWithDrawable dwin $ do
					-- Set up coordinates
					translate 0 (fromIntegral h)
					scale 1 (-1)
					scale (fromIntegral w) (fromIntegral w)
					translate 0 groundLevel

					preserve $ do
						scale scaleXY scaleXY
						translate (-shiftX) 0
						render angle (windy s garden)
					maybe (return ()) (renderMessage angle h') mbMessage
					renderTimeInfo timeInfo
					renderStats h' garden
		                  return True

	timeoutAdd (widgetQueueDraw canvas >> return True) 20

	return $ nullObserver
		{ obGrowingState = \scGen -> do
			writeIORef currentGardenRef scGen
			widgetQueueDraw canvas
		, obShutdown = mainQuit
		}

render :: Double -> AnnotatedGarden -> Render ()
render angle garden = do
	-- TODO the following can be optimized to run allKindsOfStuffWithAngle only once.
	-- by running it here. This needs modification to lightenGarden and mapLine
	renderSky angle
	--mapM_ renderLightedPoly (lightPolygons angle (gardenToLines garden))

	--mapM_ renderLightedLine (lightenLines angle (gardenToLines garden))
	--mapM_ renderLine (gardenToLines garden)
	--mapM_ renderLightedPlanted garden

	mapM_ renderPlanted garden

	renderGround

	--renderInfo garden

renderPlanted :: AnnotatedPlanted -> Render ()
renderPlanted planted = preserve $ do
	translate (plantPosition planted) 0
	setLineCap LineCapRound
	let c = colors !! fromIntegral (plantOwner planted)
	renderPlant (Just (renderFlag (take 10 (plantOwnerName planted))))
	            c (phenotype planted)

renderFlag :: String -> Render ()
renderFlag text = preserve $ do
	scale 1 (-1)
	setFontSize (groundLevel/2)
	ext <- textExtents text

	preserve $ do
		translate (stipeWidth) (groundLevel/2)
		rectangle 0
			  (textExtentsYbearing ext + groundLevel/2)
			  (textExtentsXadvance ext)
			  (-textExtentsYbearing ext - groundLevel/2 - groundLevel/2)
		setSourceRGB 1 1 1
		fill

		setSourceRGB 0 0 0
		showText text

	setLineWidth (groundLevel/10)
	setSourceRGB 0 0 0
	moveTo 0 0
	lineTo (stipeWidth + textExtentsXadvance ext) 0
	stroke


-- | Renders a plant, or part of a plant, with a given colour. If the Render
-- argument is given, it is drawn at the end of the plant, if there are no
-- branches, or passed to exactly one branch.
renderPlant :: (Maybe (Render ())) -> (Double,Double,Double) -> AnnotatedPlant -> Render ()	
renderPlant leaveR color@(r,g,b) (Plant si len ang ut ps) = preserve $ do
	rotate ang
	withLinearPattern 0 0 0 (len * stipeLength) $ \pat -> do
		let darkenByBegin = 1/(1 + (siSubLength si)/15)
		let darkenByEnd = 1/(1 + (siSubLength si - siLength si)/15)
		patternAddColorStopRGB pat 0
			(darkenByBegin*r) (darkenByBegin*g) (darkenByBegin*b) 
		patternAddColorStopRGB pat 1
			(darkenByEnd*r) (darkenByEnd*g) (darkenByEnd*b) 
		setSource pat
		--setLineWidth (stipeWidth*(0.5 + 0.5 * sqrt (siSubLength si)))
		setLineWidth stipeWidth
		moveTo 0 0
		lineTo 0 (len * stipeLength)
		stroke
	translate 0 (len * stipeLength)
	if null ps
	 then fromMaybe (return ()) leaveR
	 else sequence_ $ zipWith (\r p -> renderPlant r color p)
	                         (leaveR : repeat Nothing)
				 ps
	case siGrowth si of
	  GrowingSeed done -> do
	  	setSourceRGB 1 1 0
	  	arc 0 0 (done * blossomSize/2) 0 (2*pi)
		fill
	  _ -> return ()
		
renderLightedPlanted :: AnnotatedPlanted -> Render ()
renderLightedPlanted planted = preserve $ do
	translate (plantPosition planted) 0
	renderLightedPlant (phenotype planted)

renderLightedPlant :: AnnotatedPlant -> Render ()	
renderLightedPlant (Plant si len ang ut ps) = preserve $ do
	rotate ang
	moveTo 0 0
	lineTo 0 (len * stipeLength)
	let normalized = siLight si / (len * stipeLength)
	when (normalized > 0) $ do
		--liftIO $ print normalized
		setLineWidth (2*stipeWidth)
		setSourceRGBA 1 1 0 normalized
		stroke
	translate 0 (len * stipeLength)
	mapM_ renderLightedPlant ps
		
{- Line based rendering deprecated

renderLine (l@((x1,y1),(x2,y2)), _) = do
	setSourceRGB 0 1 0 
	setLineWidth (0.5*stipeWidth)
	moveTo x1 y1
	lineTo x2 y2
	stroke
	
renderLightedLine (l@((x1,y1),(x2,y2)), _, intensity) = do
	moveTo x1 y1
	lineTo x2 y2
	let normalized = intensity / lineLength l
	when (normalized > 0) $ do
		setLineWidth (1.5*stipeWidth)
		setSourceRGBA 1 1 0 normalized
		strokePreserve
	setSourceRGB 0 1 0 
	setLineWidth (0.5*stipeWidth)
	stroke
-}
	
renderLightedPoly ((x1,y1),(x2,y2),(x3,y3),(x4,y4), intensity) = do
	when (intensity > 0) $ do
		moveTo x1 y1
		lineTo x2 y2
		lineTo x3 y3
		lineTo x4 y4
		closePath
		setSourceRGB 0 0 intensity
		fill

renderInfo garden = do
	forM_ garden $ \planted -> do
		let x = plantPosition planted
		{-
		let text1 = printf "Light: %.2f" $
				siSubLight . pData . phenotype $ planted
		-}
		let text1 = plantOwnerName planted
		let text2 = printf "Size: %.2f" $
				siSubLength . pData . phenotype $ planted
		preserve $ do
			scale 1 (-1)
			setSourceRGB 0 0 0
			setFontSize (groundLevel/2)
			moveTo x (0.9*groundLevel)
			showText text2
			moveTo x (0.5*groundLevel)
			showText text1

renderTimeInfo timeStr = preserve $ do
		scale 1 (-1)
		setSourceRGB 0 0 0
		setFontSize (groundLevel/2)
		moveTo 0 (0.5*groundLevel)
		showText timeStr

renderMessage angle h text = preserve $ do
		scale 1 (-1)
		setSourceRGB 0 0 0
		translate (0.5) (2.5*groundLevel - h) 
		setFontSize (groundLevel)

		let bullet = " * "
		ext <- textExtents (text ++ bullet)

		rectangle (-0.25)
			  (textExtentsYbearing ext + groundLevel)
			  (0.5)
			  (-textExtentsYbearing ext - groundLevel - groundLevel)
		setSourceRGB 1 1 1
		fillPreserve
		clip

		let textWidth = textExtentsXbearing ext + textExtentsXadvance ext
		    textCount = ceiling $ 0.5/textWidth
		    scroll = 3 * (angle + pi/2)/(2*pi)
		    scroll' = scroll - fromIntegral (floor scroll)
		    scrollDist = fromIntegral textCount * textWidth
		translate (-0.25 - scroll' * scrollDist) 0

		setSourceRGB 0 0 0
		showText $ intercalate bullet $ replicate (2*textCount) text

renderStats h garden = do
	let owernerscore = foldr (\p -> M.insertWith (+) (plantOwnerName p) (plantLength (phenotype p))) M.empty garden

	setFontSize (groundLevel/2)
	let texts = map (\(n,s) -> printf "%s: %.1f" (take 20 n) s) $
			reverse $
			sortBy (comparing snd) $
		        (M.toList owernerscore)
	unless (null texts) $ preserve $ do
		scale 1 (-1)
		translate 0 (1.5*groundLevel - h) 
		
		textE <- mapM (\t -> (,) t `fmap` textExtents t) texts

		let totalHeight = groundLevel/4 + fromIntegral (length texts) * (1.0*groundLevel/2)
		let totalWidth = maximum $ map (\x -> textExtentsXadvance (snd x)) textE

		rectangle 0
			  (-1.0*groundLevel/2)
			  totalWidth
			  (totalHeight)
		setSourceRGB 1 1 1
		fill

		forM_ texts $ \text -> do
			setSourceRGB 0 0 0
			moveTo 0 0
			showText text
			translate 0 (1.0*groundLevel/2)



renderSky :: Angle -> Render ()
renderSky angle = do
	-- Clear Background
	setSourceRGB  0 0 (sin angle)
	paint

renderGround :: Render ()
renderGround = do
	setSourceRGB (140/255) (80/255) (21/255)
	rectangle (-1) 0 3 (-(1+groundLevel))
        fill

-- | Wrapper that calls 'save' and 'restore' before and after the argument
preserve :: Render () -> Render ()
preserve r = save >> r >> restore

clockTimeToDouble (TOD s p) = fromIntegral s + fromIntegral p/(1000*1000*1000*1000)