{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeFamilies #-} -- | Main module of Rasterific, an Haskell rasterization engine. -- -- Creating an image is rather simple, here is a simple example -- of a drawing and saving it in a PNG file: -- -- > import Codec.Picture( PixelRGBA8( .. ), writePng ) -- > import Graphics.Rasterific -- > -- > main :: IO () -- > main = do -- > let white = PixelRGBA8 255 255 255 255 -- > drawColor = PixelRGBA8 0 0x86 0xc1 255 -- > recColor = PixelRGBA8 0xFF 0x53 0x73 255 -- > img = renderDrawing 400 200 white $ -- > withTexture (uniformTexture drawColor) $ do -- > fill $ circle (V2 0 0) 30 -- > stroke 4 JoinRound (CapRound, CapRound) $ -- > circle (V2 400 200) 40 -- > withTexture (uniformTexture recColor) . -- > fill $ rectangle (V2 100 100) 200 100 -- > -- > writePng "yourimage.png" img -- -- <> -- -- The coordinate system is the picture classic one, with the origin in -- the upper left corner; with the y axis growing to the bottom and the -- x axis growing to the right: -- -- <> -- module Graphics.Rasterific ( -- * Rasterization command fill , withTexture , withClipping , stroke , dashedStroke , printTextAt , strokeDebug , renderDrawing , pathToPrimitives -- * Rasterization types , Texture , Drawing , Modulable -- * Geometry description , V2( .. ) , Point , Vector , CubicBezier( .. ) , Line( .. ) , Bezier( .. ) , Primitive( .. ) , Path( .. ) , PathCommand( .. ) , Transformable( .. ) -- * Helpers , line , rectangle , circle -- ** Geometry Helpers , clip , bezierFromPath , lineFromPath , cubicBezierFromPath -- * Rasterization control , Join( .. ) , Cap( .. ) , SamplerRepeat( .. ) , DashPattern ) where import Control.Applicative( (<$>) ) import Control.Monad( forM_ ) import Control.Monad.Free( Free( .. ), liftF ) import Control.Monad.ST( ST, runST ) import Control.Monad.State( StateT, execStateT, get, lift ) import Codec.Picture.Types( Image( .. ) , Pixel( .. ) , MutableImage( .. ) , createMutableImage , unsafeFreezeImage ) import qualified Data.Vector.Unboxed as VU import Linear( V2( .. ), (^+^), (^*) ) import Graphics.Rasterific.Compositor {-import Graphics.Rasterific.Operators-} import Graphics.Rasterific.Rasterize import Graphics.Rasterific.Texture import Graphics.Rasterific.Types import Graphics.Rasterific.Line import Graphics.Rasterific.QuadraticBezier import Graphics.Rasterific.CubicBezier import Graphics.Rasterific.Stroke import Graphics.Text.TrueType( Font, PointSize, getStringCurveAtPoint ) {-import Debug.Trace-} {-import Text.Printf-} -- | Monad used to describe the drawing context. type DrawContext s px a = StateT (MutableImage s px) (ST s) a ------------------------------------------------ ---- Free Monad DSL section ------------------------------------------------ -- | Monad used to record the drawing actions. type Drawing px a = Free (DrawCommand px) a data DrawCommand px next = Fill [Primitive] next | TextFill Font PointSize Point String next | SetTexture (Texture px) (Drawing px ()) next | WithCliping (forall innerPixel. Drawing innerPixel ()) (Drawing px ()) next instance Functor (DrawCommand px) where fmap f (TextFill font size pos str next) = TextFill font size pos str $ f next fmap f (Fill prims next) = Fill prims $ f next fmap f (SetTexture t sub next) = SetTexture t sub $ f next fmap f (WithCliping sub com next) = WithCliping sub com (f next) -- | Define the texture applyied to all the children -- draw call. -- -- > withTexture (uniformTexture $ PixelRGBA8 0 0x86 0xc1 255) $ do -- > fill $ circle (V2 50 50) 20 -- > fill $ circle (V2 100 100) 20 -- > withTexture (uniformTexture $ PixelRGBA8 0xFF 0x53 0x73 255) -- > $ circle (V2 150 150) 20 -- -- <> -- withTexture :: Texture px -> Drawing px () -> Drawing px () withTexture texture subActions = liftF $ SetTexture texture subActions () -- | Fill some geometry. The geometry should be "looping", -- ie. the last point of the last primitive should -- be equal to the first point of the first primitive. -- -- The primitive should be connected. -- -- > fill $ circle (V2 100 100) 75 -- -- <> -- fill :: [Primitive] -> Drawing px () fill prims = liftF $ Fill prims () -- | Draw some geometry using a clipping path. -- -- > withClipping (fill $ circle (V2 100 100) 75) $ -- > mapM_ (stroke 7 JoinRound (CapRound, CapRound)) -- > [line (V2 0 yf) (V2 200 (yf + 10)) -- > | y <- [5 :: Int, 17 .. 200] -- > , let yf = fromIntegral y ] -- -- <> -- withClipping :: (forall innerPixel. Drawing innerPixel ()) -- ^ The clipping path -> Drawing px () -- ^ The actual geometry to clip -> Drawing px () withClipping clipPath drawing = liftF $ WithCliping clipPath drawing () -- | Will stroke geometry with a given stroke width. -- The elements should be connected -- -- > stroke 5 JoinRound (CapRound, CapRound) $ circle (V2 100 100) 75 -- -- <> -- stroke :: Float -- ^ Stroke width -> Join -- ^ Which kind of join will be used -> (Cap, Cap) -- ^ Start and end capping. -> [Primitive] -- ^ List of elements to render -> Drawing px () stroke width join caping = fill . strokize width join caping -- | Draw a string at a given position. -- Text printing imply loading a font, there is no default -- font (yet). Below an example of font rendering using a -- font installed on Microsoft Windows. -- -- > import Graphics.Text.TrueType( loadFontFile ) -- > import Codec.Picture( PixelRGBA8( .. ), writePng ) -- > import Graphics.Rasterific -- > -- > main :: IO () -- > main = do -- > fontErr <- loadFontFile "C:/Windows/Fonts/arial.ttf" -- > case fontErr of -- > Left err -> putStrLn err -- > Right font -> -- > writePng "text_example.png" . -- > renderDrawing 300 70 (PixelRGBA8 255 255 255 255) -- > . withTexture (uniformTexture $ PixelRGBA8 0 0 0 255) $ -- > printTextAt font 12 (V2 20 40) "A simple text test!" -- -- <> -- -- You can use any texture, like a gradient while rendering text. -- printTextAt :: Font -- ^ Drawing font -> Int -- ^ font Point size -> Point -- ^ Baseline begining position -> String -- ^ String to print -> Drawing px () printTextAt font pointSize point string = liftF $ TextFill font pointSize point string () -- | Function to call in order to start the image creation. -- Tested pixels type are PixelRGBA8 and Pixel8, pixel types -- in other colorspace will probably produce weird results. renderDrawing :: forall px . ( Pixel px , Pixel (PixelBaseComponent px) , Modulable (PixelBaseComponent px) , PixelBaseComponent (PixelBaseComponent px) ~ (PixelBaseComponent px) ) => Int -- ^ Rendering width -> Int -- ^ Rendering height -> px -- ^ Background color -> Drawing px () -- ^ Rendering action -> Image px renderDrawing width height background drawing = runST $ createMutableImage width height background >>= execStateT (go Nothing stupidDefaultTexture drawing) >>= unsafeFreezeImage where clipBackground = emptyValue :: PixelBaseComponent px clipForeground = fullValue :: PixelBaseComponent px stupidDefaultTexture = uniformTexture $ colorMap (const clipBackground) background clipRender = renderDrawing width height clipBackground . withTexture (uniformTexture clipForeground) go :: Maybe (Texture (PixelBaseComponent px)) -> Texture px -> Drawing px () -> DrawContext s px () go _ _ (Pure ()) = return () go Nothing texture (Free (Fill prims next)) = fillWithTexture texture prims >> go Nothing texture next go mo@(Just moduler) texture (Free (Fill prims next)) = fillWithTextureAndMask texture moduler prims >> go mo texture next go moduler texture (Free (SetTexture tx sub next)) = go moduler tx sub >> go moduler texture next go moduler texture (Free (TextFill font size (V2 x y) str next)) = forM_ drawCalls (go moduler texture) >> go moduler texture next where drawCalls = beziersOfChar <$> getStringCurveAtPoint 90 (x, y) [(font, size, str)] beziersOfChar curves = liftF $ Fill bezierCurves () where bezierCurves = concat [map BezierPrim . bezierFromPath . map (uncurry V2) $ VU.toList c | c <- curves] go moduler texture (Free (WithCliping clipPath path next)) = go newModuler texture path >> go moduler texture next where modulationTexture :: Texture (PixelBaseComponent px) modulationTexture = imageTexture $ clipRender clipPath newModuler = Just $ subModuler moduler subModuler Nothing = modulationTexture subModuler (Just v) = modulateTexture v modulationTexture -- | With stroke geometry with a given stroke width, using -- a dash pattern. -- -- > dashedStroke [5, 10, 5] 3 JoinRound (CapRound, CapStraight 0) -- > [line (V2 0 100) (V2 200 100)] -- -- <> -- dashedStroke :: DashPattern -- ^ Dashing pattern to use for stroking -> Float -- ^ Stroke width -> Join -- ^ Which kind of join will be used -> (Cap, Cap) -- ^ Start and end capping. -> [Primitive] -- ^ List of elements to render -> Drawing px () dashedStroke dashing width join caping = mapM_ fill . dashedStrokize dashing width join caping -- | Internal debug function strokeDebug :: ( Pixel px, Modulable (PixelBaseComponent px)) => Texture px -> Texture px -> Float -> Join -> (Cap, Cap) -> [Primitive] -> Drawing px () strokeDebug debugPair debugImpair width join caping elems = do fill stroked forM_ (zip debugColor stroked) subStroke where stroked = strokize width join caping elems -- | Infinite list repeating color pattern debugColor = debugPair : debugImpair : debugColor subStroke (color, el) = withTexture color $ stroke 2 (JoinMiter 0) (CapStraight 0, CapStraight 0) [el] -- | Clip the geometry to a rectangle. clip :: Point -- ^ Minimum point (corner upper left) -> Point -- ^ Maximum point (corner bottom right) -> Primitive -- ^ Primitive to be clipped -> [Primitive] clip mini maxi (LinePrim l) = clipLine mini maxi l clip mini maxi (BezierPrim b) = clipBezier mini maxi b clip mini maxi (CubicBezierPrim c) = clipCubicBezier mini maxi c -- | Fill some geometry. The geometry should be "looping", -- ie. the last point of the last primitive should -- be equal to the first point of the first primitive. -- -- The primitive should be connected. fillWithTexture :: (Pixel px, Modulable (PixelBaseComponent px)) => Texture px -- ^ Color/Texture used for the filling -> [Primitive] -- ^ Primitives to fill -> DrawContext s px () fillWithTexture texture els = do img@(MutableImage width height _) <- get let mini = V2 0 0 maxi = V2 (fromIntegral width) (fromIntegral height) spans = rasterize $ els >>= clip mini maxi lift $ mapM_ (composeCoverageSpan texture img) spans fillWithTextureAndMask :: ( Pixel px , Pixel (PixelBaseComponent px) , Modulable (PixelBaseComponent px)) => Texture px -- ^ Color/Texture used for the filling -> Texture (PixelBaseComponent px) -> [Primitive] -- ^ Primitives to fill -> DrawContext s px () fillWithTextureAndMask texture mask els = do img@(MutableImage width height _) <- get let mini = V2 0 0 maxi = V2 (fromIntegral width) (fromIntegral height) spans = rasterize $ els >>= clip mini maxi lift $ mapM_ (composeCoverageSpanWithMask texture mask img) spans composeCoverageSpan :: forall s px . ( Pixel px, Modulable (PixelBaseComponent px) ) => Texture px -> MutableImage s px -> CoverageSpan -> ST s () {-# INLINE composeCoverageSpan #-} composeCoverageSpan texture img coverage | initialCov == 0 || initialX < 0 || y < 0 || imgWidth < initialX || imgHeight < y = return () | otherwise = go 0 initialX initIndex where compCount = componentCount (undefined :: px) maxi = _coverageLength coverage imgData = mutableImageData img y = floor $ _coverageY coverage initialX = floor $ _coverageX coverage imgWidth = mutableImageWidth img imgHeight = mutableImageHeight img initIndex = (initialX + y * imgWidth) * compCount (initialCov, _) = clampCoverage $ _coverageVal coverage shader = texture SamplerPad go count _ _ | count >= maxi = return () go count x idx = do oldPixel <- unsafeReadPixel imgData idx let px = shader (fromIntegral x) (fromIntegral y) opacity = pixelOpacity px (cov, icov) = coverageModulate initialCov opacity unsafeWritePixel imgData idx $ compositionAlpha cov icov oldPixel px go (count + 1) (x + 1) $ idx + compCount composeCoverageSpanWithMask :: forall s px . ( Pixel px , Pixel (PixelBaseComponent px) , Modulable (PixelBaseComponent px) ) => Texture px -> Texture (PixelBaseComponent px) -> MutableImage s px -> CoverageSpan -> ST s () {-# INLINE composeCoverageSpanWithMask #-} composeCoverageSpanWithMask texture mask img coverage | initialCov == 0 || initialX < 0 || y < 0 || imgWidth < initialX || imgHeight < y = return () | otherwise = go 0 initialX initIndex where compCount = componentCount (undefined :: px) maxi = _coverageLength coverage imgData = mutableImageData img y = floor $ _coverageY coverage initialX = floor $ _coverageX coverage imgWidth = mutableImageWidth img imgHeight = mutableImageHeight img initIndex = (initialX + y * imgWidth) * compCount (initialCov, _) = clampCoverage $ _coverageVal coverage maskShader = mask SamplerPad shader = texture SamplerPad go count _ _ | count >= maxi = return () go count x idx = do oldPixel <- unsafeReadPixel imgData idx let fx = fromIntegral x fy = fromIntegral y maskValue = maskShader fx fy px = shader fx fy (coeffMasked, _) = coverageModulate initialCov maskValue (cov, icov) = coverageModulate coeffMasked $ pixelOpacity px unsafeWritePixel imgData idx $ compositionAlpha cov icov oldPixel px go (count + 1) (x + 1) $ idx + compCount -- | Generate a list of primitive representing a circle. -- -- > fill $ circle (V2 100 100) 75 -- -- <> -- circle :: Point -- ^ Circle center in pixels -> Float -- ^ Circle radius in pixels -> [Primitive] circle center radius = CubicBezierPrim . scaleMove <$> cubicBezierCircle where mv p = (p ^* radius) ^+^ center scaleMove (CubicBezier p1 p2 p3 p4) = CubicBezier (mv p1) (mv p2) (mv p3) (mv p4) -- | Generate a list of primitive representing a -- rectangle -- -- > fill $ rectangle (V2 30 30) 150 100 -- -- <> -- rectangle :: Point -- ^ Corner upper left -> Float -- ^ Width in pixel -> Float -- ^ Height in pixel -> [Primitive] rectangle p@(V2 px py) w h = LinePrim <$> lineFromPath [ p, V2 (px + w) py, V2 (px + w) (py + h), V2 px (py + h), p ] -- | Return a simple line ready to be stroked. -- -- > stroke 17 JoinRound (CapRound, CapRound) $ -- > line (V2 10 10) (V2 180 170) -- -- <> -- line :: Point -> Point -> [Primitive] line p1 p2 = [LinePrim $ Line p1 p2]