codeworld-api-0.5.0: src/CodeWorld/CanvasM.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE JavaScriptFFI #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-
Copyright 2019 The CodeWorld Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-}
module CodeWorld.CanvasM where
import Control.Monad.Reader
import Control.Monad.Trans (MonadIO)
import Data.Text (Text)
#ifdef ghcjs_HOST_OS
import Data.JSString.Text
import GHCJS.DOM
import GHCJS.DOM.Document
import GHCJS.DOM.Element
import GHCJS.DOM.Node
import GHCJS.DOM.NonElementParentNode
import GHCJS.Types
import qualified JavaScript.Web.Canvas as Canvas
import qualified JavaScript.Web.Canvas.Internal as Canvas
#else
import Data.Text (pack)
import qualified Graphics.Blank as Canvas
import Graphics.Blank (Canvas)
import Text.Printf
#endif
class (Monad m, MonadIO m) => MonadCanvas m where
type Image m
save :: m ()
restore :: m ()
transform ::
Double -> Double -> Double -> Double -> Double -> Double -> m ()
translate :: Double -> Double -> m ()
scale :: Double -> Double -> m ()
newImage :: Int -> Int -> m (Image m)
builtinImage :: Text -> m (Maybe (Image m))
withImage :: Image m -> m a -> m a
drawImage :: Image m -> Int -> Int -> Int -> Int -> m ()
drawImgURL :: Text -> Text -> Double -> Double -> m ()
globalCompositeOperation :: Text -> m ()
globalAlpha :: Double -> m ()
lineWidth :: Double -> m ()
strokeColor :: Int -> Int -> Int -> Double -> m ()
fillColor :: Int -> Int -> Int -> Double -> m ()
font :: Text -> m ()
textCenter :: m ()
textMiddle :: m ()
beginPath :: m ()
closePath :: m ()
moveTo :: (Double, Double) -> m ()
lineTo :: (Double, Double) -> m ()
quadraticCurveTo :: (Double, Double) -> (Double, Double) -> m ()
bezierCurveTo ::
(Double, Double) -> (Double, Double) -> (Double, Double) -> m ()
arc :: Double -> Double -> Double -> Double -> Double -> Bool -> m ()
rect :: Double -> Double -> Double -> Double -> m ()
clip :: m ()
fill :: m ()
stroke :: m ()
fillRect :: Double -> Double -> Double -> Double -> m ()
fillText :: Text -> (Double, Double) -> m ()
measureText :: Text -> m Double
isPointInPath :: (Double, Double) -> m Bool
isPointInStroke :: (Double, Double) -> m Bool
getScreenWidth :: m Double
getScreenHeight :: m Double
saveRestore :: MonadCanvas m => m a -> m a
saveRestore m = do
save
r <- m
restore
return r
#if defined(ghcjs_HOST_OS)
data CanvasM a = CanvasM
{ unCanvasM :: (Double, Double) -> Canvas.Context -> IO a
} deriving (Functor)
runCanvasM :: (Double, Double) -> Canvas.Context -> CanvasM a -> IO a
runCanvasM dim ctx m = unCanvasM m dim ctx
instance Applicative CanvasM where
pure x = CanvasM (\_ _ -> return x)
f <*> x = CanvasM (\dim ctx -> unCanvasM f dim ctx <*> unCanvasM x dim ctx)
instance Monad CanvasM where
return = pure
m >>= f = CanvasM $ \dim ctx -> do
x <- unCanvasM m dim ctx
unCanvasM (f x) dim ctx
foreign import javascript "$2.globalCompositeOperation = $1;"
js_globalCompositeOperation :: JSString -> Canvas.Context -> IO ()
foreign import javascript "$2.globalAlpha = $1;"
js_globalAlpha :: Double -> Canvas.Context -> IO ()
foreign import javascript "$r = $3.isPointInPath($1, $2);"
js_isPointInPath :: Double -> Double -> Canvas.Context -> IO Bool
foreign import javascript "$r = $3.isPointInStroke($1, $2);"
js_isPointInStroke :: Double -> Double -> Canvas.Context -> IO Bool
foreign import javascript interruptible "$1.onload = $c; $1.src = $2;"
js_loadImage :: Element -> JSString -> IO ()
instance MonadIO CanvasM where
liftIO action = CanvasM $ \_ _ -> action
createOrGetImage :: Text -> Text -> IO Element
createOrGetImage name url = do
Just doc <- currentDocument
maybeImg <- getElementById doc name
case maybeImg of
Just img -> return img
Nothing -> do
img <- createElement doc (textToJSString "img")
setAttribute img (textToJSString "style") (textToJSString "display: none")
setAttribute img (textToJSString "id") name
Just body <- getBody doc
_ <- appendChild body img
js_loadImage img (textToJSString url)
return img
instance MonadCanvas CanvasM where
type Image CanvasM = Canvas.Canvas
save = CanvasM (const Canvas.save)
restore = CanvasM (const Canvas.restore)
transform a b c d e f = CanvasM (const (Canvas.transform a b c d e f))
translate x y = CanvasM (const (Canvas.translate x y))
scale x y = CanvasM (const (Canvas.scale x y))
newImage w h = liftIO (Canvas.create w h)
builtinImage name = liftIO $ do
Just doc <- currentDocument
canvas <- getElementById doc (textToJSString name)
return (Canvas.Canvas . unElement <$> canvas)
withImage img m = liftIO $ do
ctx <- Canvas.getContext img
w <- realToFrac <$> Canvas.width img
h <- realToFrac <$> Canvas.height img
unCanvasM m (w, h) ctx
drawImage (Canvas.Canvas c) x y w h =
CanvasM (const (Canvas.drawImage (Canvas.Image c) x y w h))
drawImgURL name url w h = CanvasM $ \ _ ctx -> do
img <- createOrGetImage name url
Canvas.drawImage
(Canvas.Image (unElement img))
(round (-w/2))
(round (-h/2))
(round w)
(round h)
ctx
globalCompositeOperation op =
CanvasM (const (js_globalCompositeOperation (textToJSString op)))
globalAlpha a = CanvasM (const (js_globalAlpha a))
lineWidth w = CanvasM (const (Canvas.lineWidth w))
strokeColor r g b a = CanvasM (const (Canvas.strokeStyle r g b a))
fillColor r g b a = CanvasM (const (Canvas.fillStyle r g b a))
font t = CanvasM (const (Canvas.font (textToJSString t)))
textCenter = CanvasM (const (Canvas.textAlign Canvas.Center))
textMiddle = CanvasM (const (Canvas.textBaseline Canvas.Middle))
beginPath = CanvasM (const Canvas.beginPath)
closePath = CanvasM (const Canvas.closePath)
moveTo (x, y) = CanvasM (const (Canvas.moveTo x y))
lineTo (x, y) = CanvasM (const (Canvas.lineTo x y))
quadraticCurveTo (x1, y1) (x2, y2) =
CanvasM (const (Canvas.quadraticCurveTo x1 y1 x2 y2))
bezierCurveTo (x1, y1) (x2, y2) (x3, y3) =
CanvasM (const (Canvas.bezierCurveTo x1 y1 x2 y2 x3 y3))
arc x y r a1 a2 dir = CanvasM (const (Canvas.arc x y r a1 a2 dir))
rect x y w h = CanvasM (const (Canvas.rect x y w h))
clip = CanvasM (const Canvas.clip)
fill = CanvasM (const Canvas.fill)
stroke = CanvasM (const Canvas.stroke)
fillRect x y w h = CanvasM (const (Canvas.fillRect x y w h))
fillText t (x, y) = CanvasM (const (Canvas.fillText (textToJSString t) x y))
measureText t = CanvasM (const (Canvas.measureText (textToJSString t)))
isPointInPath (x, y) = CanvasM (const (js_isPointInPath x y))
isPointInStroke (x, y) = CanvasM (const (js_isPointInStroke x y))
getScreenWidth = CanvasM $ \(w, _) _ -> return w
getScreenHeight = CanvasM $ \(_, h) _ -> return h
#else
-- Unfortunately, the Canvas monad from blank-canvas lacks a MonadIO instance.
-- We can recover it by inserting send calls where needed. This looks a lot
-- like a free monad, but we want our own interpreter logic, so it's written
-- by hand.
data CanvasM a = CanvasOp (Maybe Canvas.CanvasContext) (Canvas (CanvasM a))
| NativeOp (Canvas.DeviceContext -> IO (CanvasM a))
| PureOp a
deriving (Functor)
doCanvas :: Maybe Canvas.CanvasContext -> Canvas a -> Canvas a
doCanvas Nothing m = m
doCanvas (Just ctx) m = Canvas.with ctx m
interpCanvas :: CanvasM a -> Canvas (CanvasM a)
interpCanvas (CanvasOp mctx op) = doCanvas mctx op >>= interpCanvas
interpCanvas other = return other
runCanvasM :: Canvas.DeviceContext -> CanvasM a -> IO a
runCanvasM _ (PureOp a) = return a
runCanvasM dctx (NativeOp fm) = fm dctx >>= runCanvasM dctx
runCanvasM dctx m = Canvas.send dctx (interpCanvas m) >>= runCanvasM dctx
instance Applicative CanvasM where
pure = PureOp
(CanvasOp mctx1 f) <*> (CanvasOp mctx2 x) = CanvasOp mctx1 (fmap (<*>) f <*> doCanvas mctx2 x)
f <*> x = f `ap` x
instance Monad CanvasM where
return = pure
PureOp x >>= f = f x
NativeOp op >>= f = NativeOp $ \dctx -> do
next <- op dctx
return (next >>= f)
CanvasOp mctx op >>= f = CanvasOp mctx $ bindCanvas (doCanvas mctx op) f
bindCanvas :: Canvas (CanvasM a) -> (a -> CanvasM b) -> Canvas (CanvasM b)
bindCanvas m cont = do
next <- m
case next of
CanvasOp mctx op -> bindCanvas (doCanvas mctx op) cont
_ -> return (next >>= cont)
instance MonadIO CanvasM where
liftIO x = NativeOp $ const $ PureOp <$> x
liftCanvas :: Canvas a -> CanvasM a
liftCanvas m = CanvasOp Nothing (PureOp <$> m)
instance MonadCanvas CanvasM where
type Image CanvasM = Canvas.CanvasContext
save = liftCanvas $ Canvas.save ()
restore = liftCanvas $ Canvas.restore ()
transform a b c d e f = liftCanvas $ Canvas.transform (a, b, c, d, e, f)
translate x y = liftCanvas $ Canvas.translate (x, y)
scale x y = liftCanvas $ Canvas.scale (x, y)
newImage w h = liftCanvas $ Canvas.newCanvas (w, h)
builtinImage _name = return Nothing
withImage ctx (CanvasOp Nothing m) = CanvasOp (Just ctx) m
withImage _ (CanvasOp mctx m) = CanvasOp mctx m
withImage ctx (NativeOp fm) = NativeOp $ \dctx -> withImage ctx <$> fm dctx
withImage _ (PureOp x) = PureOp x
drawImage img x y w h = liftCanvas $
Canvas.drawImageSize
( img
, fromIntegral x
, fromIntegral y
, fromIntegral w
, fromIntegral h)
drawImgURL _name _url _w _h = return ()
globalCompositeOperation op = liftCanvas $ Canvas.globalCompositeOperation op
globalAlpha a = liftCanvas $ Canvas.globalAlpha a
lineWidth w = liftCanvas $ Canvas.lineWidth w
strokeColor r g b a = liftCanvas $ Canvas.strokeStyle
(pack (printf "rgba(%d,%d,%d,%.2f)" r g b a))
fillColor r g b a = liftCanvas $ Canvas.fillStyle
(pack (printf "rgba(%d,%d,%d,%.2f)" r g b a))
font t = liftCanvas $ Canvas.font t
textCenter = liftCanvas $ Canvas.textAlign Canvas.CenterAnchor
textMiddle = liftCanvas $ Canvas.textBaseline Canvas.MiddleBaseline
beginPath = liftCanvas $ Canvas.beginPath ()
closePath = liftCanvas $ Canvas.closePath ()
moveTo (x, y) = liftCanvas $ Canvas.moveTo (x, y)
lineTo (x, y) = liftCanvas $ Canvas.lineTo (x, y)
quadraticCurveTo (x1, y1) (x2, y2) = liftCanvas $
Canvas.quadraticCurveTo (x1, y1, x2, y2)
bezierCurveTo (x1, y1) (x2, y2) (x3, y3) = liftCanvas $
Canvas.bezierCurveTo (x1, y1, x2, y2, x3, y3)
arc x y r a1 a2 dir = liftCanvas $
Canvas.arc (x, y, r, a1, a2, dir)
rect x y w h = liftCanvas $ Canvas.rect (x, y, w, h)
clip = liftCanvas $ Canvas.clip ()
fill = liftCanvas $ Canvas.fill ()
stroke = liftCanvas $ Canvas.stroke ()
fillRect x y w h = liftCanvas $ Canvas.fillRect (x, y, w, h)
fillText t (x, y) = liftCanvas $ Canvas.fillText (t, x, y)
measureText t = liftCanvas $ do
Canvas.TextMetrics w <- Canvas.measureText t
return w
isPointInPath (x, y) = liftCanvas $ Canvas.isPointInPath (x, y)
isPointInStroke _ = liftCanvas $ return False
getScreenWidth = liftCanvas $ Canvas.width <$> Canvas.myCanvasContext
getScreenHeight = liftCanvas $ Canvas.height <$> Canvas.myCanvasContext
#endif