module Graphics.Blank
(
blankCanvas
, Options(..)
, DeviceContext
, send
, Canvas
, height
, width
, toDataURL
, save
, restore
, scale
, rotate
, translate
, transform
, setTransform
, Image
, drawImage
, globalAlpha
, globalCompositeOperation
, lineWidth
, lineCap
, lineJoin
, miterLimit
, strokeStyle
, fillStyle
, shadowOffsetX
, shadowOffsetY
, shadowBlur
, shadowColor
, createLinearGradient
, createRadialGradient
, createPattern
, addColorStop
, CanvasGradient
, CanvasPattern
, beginPath
, closePath
, fill
, stroke
, clip
, moveTo
, lineTo
, quadraticCurveTo
, bezierCurveTo
, arcTo
, arc
, rect
, isPointInPath
, font
, textAlign
, textBaseline
, fillText
, strokeText
, measureText
, TextMetrics(..)
, clearRect
, fillRect
, strokeRect
, getImageData
, putImageData
, ImageData(..)
, newImage
, CanvasImage
, devicePixelRatio
, CanvasContext
, newCanvas
, with
, myCanvasContext
, deviceCanvasContext
, console_log
, eval
, JSArg(..)
, module Graphics.Blank.Utils
, trigger
, eventQueue
, wait
, flush
, Event(..)
, EventName
, EventQueue
, local_only
) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad
import Control.Exception
import Network.Wai.Handler.Warp (run)
import Network.Wai (Middleware,remoteHost, responseLBS)
import qualified Network.HTTP.Types as H
import Network.Socket (SockAddr(..))
import System.IO.Unsafe (unsafePerformIO)
import Web.Scotty (scottyApp, get, file)
import qualified Web.Scotty as Scotty
import qualified Web.Scotty.Comet as KC
import Data.Aeson
import Data.Aeson.Types (parse)
import Data.String
import qualified Data.Text as T
import Data.Text (Text)
import Data.Monoid((<>))
import Graphics.Blank.Events
import Graphics.Blank.DeviceContext
import Graphics.Blank.Canvas
import Graphics.Blank.Generated hiding (fillStyle,strokeStyle)
import qualified Graphics.Blank.Generated as Generated
import qualified Graphics.Blank.JavaScript as JavaScript
import Graphics.Blank.JavaScript hiding (width, height)
import Graphics.Blank.Utils
import Paths_blank_canvas
blankCanvas :: Options -> (DeviceContext -> IO ()) -> IO ()
blankCanvas opts actions = do
dataDir <- getDataDir
kComet <- KC.kCometPlugin
app <- scottyApp $ do
sequence_ [ Scotty.middleware ware
| ware <- middleware opts
]
let kc_opts :: KC.Options
kc_opts = KC.Options { KC.prefix = "/blank", KC.verbose = if debug opts then 3 else 0 }
KC.connect kc_opts $ \ kc_doc -> do
KC.send kc_doc $ T.unlines
[ "register(" <> T.pack(show nm) <> ");"
| nm <- events opts
]
queue <- atomically newTChan
_ <- forkIO $ forever $ do
val <- atomically $ readTChan $ KC.eventQueue $ kc_doc
case fromJSON val of
Success (event :: Event) -> do
atomically $ writeTChan queue event
_ -> return ()
let cxt0 = DeviceContext kc_doc queue 300 300 1
DeviceAttributes w h dpr <- send cxt0 device
let cxt1 = cxt0
{ ctx_width = w
, ctx_height = h
, ctx_devicePixelRatio = dpr
}
(actions $ cxt1) `catch` \ (e :: SomeException) -> do
print ("Exception in blank-canvas application:" :: String)
print e
throw e
get "/" $ file $ dataDir ++ "/static/index.html"
get "/jquery.js" $ file $ dataDir ++ "/static/jquery.js"
get "/jquery-json.js" $ file $ dataDir ++ "/static/jquery-json.js"
get "/kansas-comet.js" $ file $ kComet
sequence_ [ get (fromString ("/" ++ nm)) $ file $ (root opts ++ "/" ++ nm) | nm <- static opts ]
return ()
run (port opts) app
send :: DeviceContext -> Canvas a -> IO a
send cxt commands =
send' (deviceCanvasContext cxt) commands id
where
send' :: CanvasContext -> Canvas a -> (String -> String) -> IO a
send' c (Bind (Return a) k) cmds = send' c (k a) cmds
send' c (Bind (Bind m k1) k2) cmds = send' c (Bind m (\ r -> Bind (k1 r) k2)) cmds
send' c (Bind (Method cmd) k) cmds = send' c (k ()) (cmds . ((showJS c ++ ".") ++) . shows cmd . (";" ++))
send' c (Bind (Command cmd) k) cmds = send' c (k ()) (cmds . shows cmd . (";" ++))
send' c (Bind (Query query) k) cmds = do
uq <- atomically $ getUniq
sendToCanvas cxt (cmds . ((show query ++ "(" ++ show uq ++ "," ++ showJS c ++ ");") ++))
v <- KC.getReply (theComet cxt) uq
case parse (parseQueryResult query) v of
Error msg -> fail msg
Success a -> do
send' c (k a) id
send' c (Bind (With c' m) k) cmds = send' c' (Bind m (With c . k)) cmds
send' c (Bind MyContext k) cmds = send' c (k c) cmds
send' _ (With c m) cmds = send' c m cmds
send' c MyContext cmds = send' c (Return c) cmds
send' _ (Return a) cmds = do
sendToCanvas cxt cmds
return a
send' c cmd cmds = send' c (Bind cmd Return) cmds
local_only :: Middleware
local_only f r k = case remoteHost r of
SockAddrInet _ h | h == fromIntegral home
-> f r k
#if !defined(mingw32_HOST_OS) && !defined(_WIN32)
SockAddrUnix _ -> f r k
#endif
_ -> k $ responseLBS H.status403
[("Content-Type", "text/plain")]
"local access only"
where
home :: Integer
home = 127 + (256 * 256 * 256) * 1
uniqVar :: TVar Int
uniqVar = unsafePerformIO $ newTVarIO 0
getUniq :: STM Int
getUniq = do
u <- readTVar uniqVar
writeTVar uniqVar (u + 1)
return u
data Options = Options
{ port :: Int
, events :: [EventName]
, debug :: Bool
, static :: [String]
, root :: String
, middleware :: [Middleware]
}
instance Num Options where
(+) = error "no arithmetic for Blank Canvas Options"
() = error "no arithmetic for Blank Canvas Options"
(*) = error "no arithmetic for Blank Canvas Options"
abs = error "no arithmetic for Blank Canvas Options"
signum = error "no arithmetic for Blank Canvas Options"
fromInteger n = Options { port = fromInteger n
, events = []
, debug = False
, static = []
, root = "."
, middleware = [local_only]
}
fillStyle :: Text -> Canvas ()
fillStyle = Generated.fillStyle
strokeStyle :: Text -> Canvas ()
strokeStyle = Generated.strokeStyle
height :: (Image image, Num a) => image -> a
height = JavaScript.height
width :: (Image image, Num a) => image -> a
width = JavaScript.width