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
, LineEndCap(..)
, butt
, square
, LineJoinCorner(..)
, bevel
, miter
, strokeStyle
, fillStyle
, shadowOffsetX
, shadowOffsetY
, shadowBlur
, shadowColor
, createLinearGradient
, createRadialGradient
, createPattern
, addColorStop
, RepeatDirection(..)
, repeat_
, repeatX
, repeatY
, noRepeat
, CanvasGradient
, CanvasPattern
, beginPath
, closePath
, fill
, stroke
, clip
, moveTo
, lineTo
, quadraticCurveTo
, bezierCurveTo
, arcTo
, arc
, rect
, isPointInPath
, font
, textAlign
, textBaseline
, fillText
, strokeText
, measureText
, TextAnchorAlignment(..)
, start
, end
, center
, left
, right
, TextBaselineAlignment(..)
, top
, hanging
, middle
, alphabetic
, ideographic
, bottom
, TextMetrics(..)
, clearRect
, fillRect
, strokeRect
, getImageData
, putImageData
, ImageData(..)
, Alpha
, Degrees
, Interval
, Percentage
, Radians
, RoundProperty(..)
, newImage
, CanvasImage
, newAudio
, CanvasAudio
, devicePixelRatio
, CanvasContext
, newCanvas
, with
, myCanvasContext
, deviceCanvasContext
, sync
, console_log
, eval
, JSArg(..)
, module Graphics.Blank.Utils
, trigger
, eventQueue
, wait
, flush
, Event(..)
, EventName
, EventQueue
, cursor
, local_only
) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad (forever)
import Control.Monad.IO.Class
import Data.Aeson
import Data.Aeson.Types (parse)
import Data.List as L
import qualified Data.Map as M (lookup)
import Data.Monoid ((<>))
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.Lazy as LT
import qualified Graphics.Blank.Canvas as Canvas
import Graphics.Blank.Canvas hiding (addColorStop, cursor)
import Graphics.Blank.DeviceContext
import Graphics.Blank.Events
import qualified Graphics.Blank.Generated as Generated
import Graphics.Blank.Generated hiding (fillStyle, font, strokeStyle, shadowColor)
import qualified Graphics.Blank.JavaScript as JavaScript
import Graphics.Blank.JavaScript hiding (width, height)
import Graphics.Blank.Types
import Graphics.Blank.Utils
import qualified Network.HTTP.Types as H
import Network.Mime (defaultMimeMap, fileNameExtensions)
import Network.Wai (Middleware, responseLBS)
import Network.Wai.Middleware.Local
import Network.Wai.Handler.Warp
import Paths_blank_canvas
import Prelude.Compat
import System.IO.Unsafe (unsafePerformIO)
import TextShow (Builder, showb, showt, singleton)
import qualified Web.Scotty as Scotty
import Web.Scotty (scottyApp, get, file)
import qualified Web.Scotty.Comet as KC
blankCanvas :: Options -> (DeviceContext -> IO ()) -> IO ()
blankCanvas opts actions = do
dataDir <- getDataDir
kComet <- KC.kCometPlugin
locals :: TVar (S.Set Text) <- atomically $ newTVar S.empty
let kc_opts :: KC.Options
kc_opts = KC.Options { KC.prefix = "/blank", KC.verbose = if debug opts then 3 else 0 }
connectApp <- KC.connect kc_opts $ \ kc_doc -> do
KC.send kc_doc $ T.unlines
[ "register(" <> showt 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 locals False
DeviceAttributes w h dpr <- send cxt0 device
let cxt1 = cxt0
{ ctx_width = w
, ctx_height = h
, ctx_devicePixelRatio = dpr
, weakRemoteMonad = weak opts
}
(actions $ cxt1) `catch` \ (e :: SomeException) -> do
print ("Exception in blank-canvas application:" :: String)
print e
throw e
app <- scottyApp $ do
sequence_ [ Scotty.middleware ware
| ware <- middleware opts
]
connectApp
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
get (Scotty.regex "^/(.*)$") $ do
fileName :: Text <- Scotty.param "1"
db <- liftIO $ atomically $ readTVar $ locals
if fileName `S.member` db
then do
let mime = mimeType fileName
Scotty.setHeader "Content-Type" $ LT.fromStrict $ mime
file $ (root opts ++ "/" ++ T.unpack fileName)
else do
Scotty.next
return ()
runSettings (setPort (port opts)
$ setTimeout 5
$ defaultSettings
) app
send :: DeviceContext -> Canvas a -> IO a
send _ (Return a) = return a
send cxt (Bind m k) | weakRemoteMonad cxt = send cxt m >>= send cxt . k
send cxt (With c (Bind m k)) | weakRemoteMonad cxt = send cxt (With c m) >>= send cxt . With c . k
send cxt (With _ (With c m)) | weakRemoteMonad cxt = send cxt (With c m)
send cxt commands = send' (deviceCanvasContext cxt) commands mempty
where
sendBind :: CanvasContext -> Canvas a -> (a -> Canvas b) -> Builder -> IO b
sendBind c (Return a) k cmds = send' c (k a) cmds
sendBind c (Bind m k1) k2 cmds = sendBind c m (\ r -> Bind (k1 r) k2) cmds
sendBind c (Method cmd) k cmds = send' c (k ()) (cmds <> jsCanvasContext c <> singleton '.' <> showb cmd <> singleton ';')
sendBind c (Command cmd) k cmds = send' c (k ()) (cmds <> showb cmd <> singleton ';')
sendBind c (Function func) k cmds = sendFunc c func k cmds
sendBind c (Query query) k cmds = sendQuery c query k cmds
sendBind c (With c' m) k cmds = send' c' (Bind m (With c . k)) cmds
sendBind c MyContext k cmds = send' c (k c) cmds
sendFunc :: CanvasContext -> Function a -> (a -> Canvas b) -> Builder -> IO b
sendFunc c q@(CreateLinearGradient _) k cmds = sendGradient c q k cmds
sendFunc c q@(CreateRadialGradient _) k cmds = sendGradient c q k cmds
sendFunc c q@(CreatePattern _) k cmds = sendPattern c q k cmds
sendGradient :: CanvasContext -> Function a -> (CanvasGradient -> Canvas b) -> Builder -> IO b
sendGradient c q k cmds = do
gId <- atomically getUniq
send' c (k $ CanvasGradient gId) $ cmds <> "var gradient_"
<> showb gId <> " = " <> jsCanvasContext c
<> singleton '.' <> showb q <> singleton ';'
sendPattern :: CanvasContext -> Function a -> (CanvasPattern -> Canvas b) -> Builder -> IO b
sendPattern c q k cmds = do
pId <- atomically getUniq
send' c (k $ CanvasPattern pId) $ cmds <> "var pattern_"
<> showb pId <> " = " <> jsCanvasContext c
<> singleton '.' <> showb q <> singleton ';'
fileQuery :: Text -> IO ()
fileQuery url = do
let url' = if "/" `T.isPrefixOf` url then T.tail url else url
atomically $ do
db <- readTVar (localFiles cxt)
writeTVar (localFiles cxt) $ S.insert url' $ db
sendQuery :: CanvasContext -> Query a -> (a -> Canvas b) -> Builder -> IO b
sendQuery c query k cmds = do
case query of
NewImage url -> fileQuery url
NewAudio url -> fileQuery url
_ -> return ()
uq <- atomically $ getUniq
sendToCanvas cxt $ cmds <> showb query <> singleton '(' <> showb uq <> singleton ',' <> jsCanvasContext c <> ");"
v <- KC.getReply (theComet cxt) uq
case parse (parseQueryResult query) v of
Error msg -> fail msg
Success a -> send' c (k a) mempty
send' :: CanvasContext -> Canvas a -> Builder -> IO a
send' c (Bind m k) cmds = sendBind c m k cmds
send' _ (With c m) cmds = send' c m cmds
send' _ (Return a) cmds = do
sendToCanvas cxt cmds
return a
send' c cmd cmds = sendBind c cmd Return cmds
local_only :: Middleware
local_only = local $ responseLBS H.status403 [("Content-Type", "text/plain")] "local access only"
uniqVar :: TVar Int
uniqVar = unsafePerformIO $ newTVarIO 0
getUniq :: STM Int
getUniq = do
u <- readTVar uniqVar
writeTVar uniqVar (u + 1)
return u
mimeType :: Text -> Text
mimeType filePath = go $ fileNameExtensions filePath
where
go [] = error $ "do not understand mime type for : " ++ show filePath
go (e:es) = case M.lookup e defaultMimeMap of
Nothing -> go es
Just mt -> decodeUtf8 mt
data Options = Options
{ port :: Int
, events :: [EventName]
, debug :: Bool
, root :: String
, middleware :: [Middleware]
, weak :: Bool
}
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
, root = "."
, middleware = [local_only]
, weak = False
}
fillStyle :: Text -> Canvas ()
fillStyle = Generated.fillStyle
font :: Text -> Canvas ()
font = Generated.font
strokeStyle :: Text -> Canvas ()
strokeStyle = Generated.strokeStyle
shadowColor :: Text -> Canvas ()
shadowColor = Generated.shadowColor
addColorStop :: (Interval, Text) -> CanvasGradient -> Canvas ()
addColorStop = Canvas.addColorStop
cursor :: Text -> Canvas ()
cursor = Canvas.cursor
height :: (Image image, Num a) => image -> a
height = JavaScript.height
width :: (Image image, Num a) => image -> a
width = JavaScript.width