blank-canvas-0.4.0: Graphics/Blank/Canvas.hs
{-# LANGUAGE TemplateHaskell, GADTs, KindSignatures, ScopedTypeVariables, OverloadedStrings, FlexibleInstances, OverlappingInstances #-}
module Graphics.Blank.Canvas where
import Graphics.Blank.Events
import Graphics.Blank.JavaScript
import Data.Aeson (FromJSON(..),Value(..),encode)
import Data.Aeson.Types (Parser, (.:))
import Data.Char (chr)
import Control.Monad (ap, liftM2)
import Control.Applicative
import Data.Monoid
import qualified Data.ByteString.Lazy as DBL
import qualified Data.Text as Text
import Data.Text (Text)
data Canvas :: * -> * where
Method :: Method -> Canvas () -- <context>.<method>
Command :: Command -> Canvas () -- <command>
Query :: (Show a) => Query a -> Canvas a
With :: CanvasContext -> Canvas a -> Canvas a
MyContext :: Canvas CanvasContext
Bind :: Canvas a -> (a -> Canvas b) -> Canvas b
Return :: a -> Canvas a
instance Monad Canvas where
return = Return
(>>=) = Bind
instance Applicative Canvas where
pure = return
(<*>) = ap
instance Functor Canvas where
fmap f c = c >>= return . f
instance Monoid a => Monoid (Canvas a) where
mappend = liftM2 mappend
mempty = return mempty
-- HTML5 Canvas assignments: FillStyle, Font, GlobalAlpha, GlobalCompositeOperation, LineCap, LineJoin, LineWidth, MiterLimit, ShadowBlur, ShadowColor, ShadowOffsetX, ShadowOffsetY, StrokeStyle, TextAlign, TextBaseline
data Method
-- regular HTML5 canvas commands
= Arc (Float,Float,Float,Float,Float,Bool)
| ArcTo (Float,Float,Float,Float,Float)
| BeginPath
| BezierCurveTo (Float,Float,Float,Float,Float,Float)
| forall image . Image image => DrawImage (image,[Float]) -- 'drawImage' takes 2, 4 or 8 floats arguments
| ClearRect (Float,Float,Float,Float)
| Clip
| ClosePath
| Fill
| FillRect (Float,Float,Float,Float)
| forall style . Style style => FillStyle style
| FillText (Text,Float,Float)
| Font Text
| GlobalAlpha Float
| GlobalCompositeOperation Text
| LineCap Text
| LineJoin Text
| LineTo (Float,Float)
| LineWidth Float
| MiterLimit Float
| MoveTo (Float,Float)
| PutImageData (ImageData,[Float])
| QuadraticCurveTo (Float,Float,Float,Float)
| Rect (Float,Float,Float,Float)
| Restore
| Rotate Float
| Scale (Float,Float)
| Save
| SetTransform (Float,Float,Float,Float,Float,Float)
| Stroke
| StrokeRect (Float,Float,Float,Float)
| StrokeText (Text,Float,Float)
| forall style . Style style => StrokeStyle style
| ShadowBlur Float
| ShadowColor Text
| ShadowOffsetX Float
| ShadowOffsetY Float
| TextAlign Text
| TextBaseline Text
| Transform (Float,Float,Float,Float,Float,Float)
| Translate (Float,Float)
data Command
= Trigger Event
| AddColorStop (Float,Text) CanvasGradient
| forall msg . JSArg msg => Log msg
| Eval Text
instance Show Command where
show (Trigger e) = "Trigger(" ++ map (chr . fromEnum) (DBL.unpack (encode e)) ++ ")"
show (AddColorStop (off,rep) g)
= showJS g ++ ".addColorStop(" ++ showJS off ++ "," ++ showJS rep ++ ")"
show (Log msg) = "console.log(" ++ showJS msg ++ ")"
show (Eval cmd) = Text.unpack cmd -- no escaping or interpretation
-----------------------------------------------------------------------------
-- | 'with' runs a set of canvas commands in the context
-- of a specific canvas buffer.
with :: CanvasContext -> Canvas a -> Canvas a
with = With
-- | 'myCanvasContext' returns the current 'CanvasContent'.
myCanvasContext :: Canvas CanvasContext
myCanvasContext = MyContext
-----------------------------------------------------------------------------
-- | trigger a specific named event, please.
trigger :: Event -> Canvas ()
trigger = Command . Trigger
-- | add a Color stop to a Canvas Gradient.
addColorStop :: (Float,Text) -> CanvasGradient -> Canvas ()
addColorStop (off,rep) = Command . AddColorStop (off,rep)
-- | 'console_log' aids debugging by sending the argument to the browser console.log.
console_log :: JSArg msg => msg -> Canvas ()
console_log = Command . Log
-- | 'eval' executes the argument in JavaScript directly.
eval :: Text -> Canvas ()
eval = Command . Eval
-----------------------------------------------------------------------------
data Query :: * -> * where
Device :: Query DeviceAttributes
ToDataURL :: Query Text
MeasureText :: Text -> Query TextMetrics
IsPointInPath :: (Float,Float) -> Query Bool
NewImage :: Text -> Query CanvasImage
CreateLinearGradient :: (Float,Float,Float,Float) -> Query CanvasGradient
CreateRadialGradient :: (Float,Float,Float,Float,Float,Float) -> Query CanvasGradient
CreatePattern :: Image image => (image,Text) -> Query CanvasPattern
NewCanvas :: (Int,Int) -> Query CanvasContext
GetImageData :: (Float,Float,Float,Float) -> Query ImageData
data DeviceAttributes = DeviceAttributes Int Int Float
deriving Show
-- | The 'width' argument of 'TextMetrics' can trivially be projected out.
data TextMetrics = TextMetrics Float
deriving Show
instance Show (Query a) where
show Device = "Device"
show ToDataURL = "ToDataURL"
show (MeasureText txt) = "MeasureText(" ++ showJS txt ++ ")"
show (IsPointInPath (x,y)) = "IsPointInPath(" ++ showJS x ++ "," ++ showJS y ++ ")"
show (NewImage url) = "NewImage(" ++ showJS url ++ ")"
show (CreateLinearGradient (x0,y0,x1,y1)) = "CreateLinearGradient(" ++ showJS x0 ++ "," ++ showJS y0 ++ "," ++ showJS x1 ++ "," ++ showJS y1 ++ ")"
show (CreateRadialGradient (x0,y0,r0,x1,y1,r1)) = "CreateRadialGradient(" ++ showJS x0 ++ "," ++ showJS y0 ++ "," ++ showJS r0 ++ "," ++ showJS x1 ++ "," ++ showJS y1 ++ "," ++ showJS r1 ++ ")"
show (CreatePattern (img,str)) = "CreatePattern(" ++ jsImage img ++ "," ++ showJS str ++ ")"
show (NewCanvas (x,y)) = "NewCanvas(" ++ showJS x ++ "," ++ showJS y ++ ")"
show (GetImageData (sx,sy,sw,sh))
= "GetImageData(" ++ showJS sx ++ "," ++ showJS sy ++ "," ++ showJS sw ++ "," ++ showJS sh ++ ")"
-- This is how we take our value to bits
parseQueryResult :: Query a -> Value -> Parser a
parseQueryResult (Device {}) o = uncurry3 DeviceAttributes <$> parseJSON o
parseQueryResult (ToDataURL {}) o = parseJSON o
parseQueryResult (MeasureText {}) (Object v) = TextMetrics <$> v .: "width"
parseQueryResult (IsPointInPath {}) o = parseJSON o
parseQueryResult (NewImage {}) o = uncurry3 CanvasImage <$> parseJSON o
parseQueryResult (CreateLinearGradient {}) o = CanvasGradient <$> parseJSON o
parseQueryResult (CreateRadialGradient {}) o = CanvasGradient <$> parseJSON o
parseQueryResult (CreatePattern {}) o = CanvasPattern <$> parseJSON o
parseQueryResult (NewCanvas {}) o = uncurry3 CanvasContext <$> parseJSON o
parseQueryResult (GetImageData {}) (Object o) = ImageData
<$> (o .: "width")
<*> (o .: "height")
<*> (o .: "data")
parseQueryResult _ _ = fail "no parse in blank-canvas server (internal error)"
uncurry3 :: (t0 -> t1 -> t2 -> t3) -> (t0, t1, t2) -> t3
uncurry3 f (a,b,c) = f a b c
device :: Canvas DeviceAttributes
device = Query Device
-- | Turn the canvas into a png data stream / data URL.
--
-- > "data:image/png;base64,iVBORw0KGgo.."
--
toDataURL :: () -> Canvas Text
toDataURL () = Query ToDataURL
measureText :: Text -> Canvas TextMetrics
measureText = Query . MeasureText
isPointInPath :: (Float,Float) -> Canvas Bool
isPointInPath = Query . IsPointInPath
-- | 'image' takes a URL (perhaps a data URL), and returns the 'CanvasImage' handle,
-- _after_ loading.
-- The assumption is you are using local images, so loading should be near instant.
newImage :: Text -> Canvas CanvasImage
newImage = Query . NewImage
createLinearGradient :: (Float,Float,Float,Float) -> Canvas CanvasGradient
createLinearGradient = Query . CreateLinearGradient
createRadialGradient :: (Float,Float,Float,Float,Float,Float) -> Canvas CanvasGradient
createRadialGradient = Query . CreateRadialGradient
createPattern :: (CanvasImage, Text) -> Canvas CanvasPattern
createPattern = Query . CreatePattern
-- | Create a new, off-screen canvas buffer. Takes width and height.
newCanvas :: (Int,Int) -> Canvas CanvasContext
newCanvas = Query . NewCanvas
-- | Capture ImageDate from the Canvas.
getImageData :: (Float,Float,Float,Float) -> Canvas ImageData
getImageData = Query . GetImageData