packages feed

canvhs-0.2.0.0: src/Graphics/CanvHs/Picture.hs

--
-- A lot of this code was written by Gemini
--
module Graphics.CanvHs.Picture(
  Picture(..),
  MouseState(..),
  display,
  animate,
  playWithSound,
  play,
  reset,
  ) where
import Control.Exception(bracket_)
import Foreign.C.String(withCAString, CString)
import Graphics.CanvHs.Color
import Audio.AudHs.Sound

-- | Standard Gloss/Shine Picture primitives
data Picture
  = Blank
  | Line [(Double, Double)]          -- A stroked path
  | Polygon [(Double, Double)]       -- A filled polygon
  | Circle Double                    -- A stroked circle
  | SolidCircle Double               -- A filled circle
  | Rectangle Double Double          -- Width, Height (centered)
  | SolidRectangle Double Double     -- Width, Height (centered)
  | Color Color Picture              -- Applies color to child
  | Translate Double Double Picture  -- X, Y shift
  | Rotate Double Picture            -- Angle in degrees (clockwise)
  | Scale Double Double Picture      -- X, Y scaling
  | Pictures [Picture]               -- Group of pictures
  | Text String                      -- text

data MouseState = MouseState
  { mouseX      :: Double
  , mouseY      :: Double
  , mouseIsDown :: Bool
  } deriving (Show, Eq)

------------------------------------------------------------
-- JavaScript FFI Bindings

foreign import javascript unsafe "window.ctx.save();"
  js_save :: IO ()

foreign import javascript unsafe "window.ctx.restore();"
  js_restore :: IO ()

foreign import javascript unsafe "window.ctx.translate($0, $1);"
  js_translate :: Double -> Double -> IO ()

foreign import javascript unsafe "window.ctx.rotate($0);"
  js_rotate :: Double -> IO ()

foreign import javascript unsafe "window.ctx.scale($0, $1);"
  js_scale :: Double -> Double -> IO ()

foreign import javascript unsafe "window.ctx.beginPath();"
  js_beginPath :: IO ()

foreign import javascript unsafe "window.ctx.closePath();"
  js_closePath :: IO ()

foreign import javascript unsafe "window.ctx.moveTo($0, $1);"
  js_moveTo :: Double -> Double -> IO ()

foreign import javascript unsafe "window.ctx.lineTo($0, $1);"
  js_lineTo :: Double -> Double -> IO ()

foreign import javascript unsafe "window.ctx.fill();"
  js_fill :: IO ()

foreign import javascript unsafe "window.ctx.stroke();"
  js_stroke :: IO ()

foreign import javascript unsafe "var c = window.ctx; c.arc(0, 0, $0, 0, 2 * Math.PI); c.stroke();"
  js_strokeCircle :: Double -> IO ()

foreign import javascript unsafe "var c = window.ctx; c.arc(0, 0, $0, 0, 2 * Math.PI); c.fill();"
  js_fillCircle :: Double -> IO ()

foreign import javascript unsafe "var c = window.ctx; c.rect(-($0/2), -($1/2), $0, $1); c.stroke();"
  js_strokeRect :: Double -> Double -> IO ()

foreign import javascript unsafe "var c = window.ctx; c.rect(-($0/2), -($1/2), $0, $1); c.fill();"
  js_fillRect :: Double -> Double -> IO ()

foreign import javascript unsafe "var col = 'rgba(' + Math.round($0*255) + ',' + Math.round($1*255) + ',' + Math.round($2*255) + ',' + $3 + ')'; var c = window.ctx; c.fillStyle = col; c.strokeStyle = col;"
  js_setColor :: Double -> Double -> Double -> Double -> IO ()

foreign import javascript unsafe "window.ctx.clearRect(0, 0, window.cvs.width, window.cvs.height);"
  js_clearCanvas :: IO ()

foreign import javascript unsafe "var c = window.ctx; c.translate(window.cvs.width / 2, window.cvs.height / 2); c.scale(1, -1);"
  js_setupCoordinates :: IO ()

foreign import javascript unsafe "window.showCanvas();"
  js_showCanvas :: IO ()

-- We save, flip Y back to normal, draw the text, and restore.
foreign import javascript unsafe "window.drawHaskellText($0);"
  js_fillText :: CString -> IO ()

foreign import javascript unsafe "window.ctx.resetTransform();"
  js_resetTransform :: IO ()

foreign import javascript unsafe "console.log('js_log=', $0);"
  js_log :: Double -> IO ()

foreign import javascript unsafe "return performance.now()"
  js_now :: IO Double

foreign import javascript unsafe "return window.mouseState.x;"
  js_mouseX :: IO Double

foreign import javascript unsafe "return window.mouseState.y;"
  js_mouseY :: IO Double

foreign import javascript unsafe "return window.mouseState.down;"
  js_mouseDown :: IO Int

foreign import ccall unsafe "emscripten_sleep"
  emscripten_sleep :: Int -> IO ()

foreign import capi "value extern void c_begin_draw(void); c_begin_draw()"
  c_begin_draw :: IO ()

foreign import capi "value extern void c_end_draw(void); c_end_draw()"
  c_end_draw :: IO ()

foreign import capi "value extern void c_waitForFrame(void); c_waitForFrame()"
  js_waitForFrame :: IO ()

----------------------------------------------------------------

{-
withSaved :: IO () -> IO ()
withSaved = bracket_ js_save js_restore

withDrawLock :: IO a -> IO a
withDrawLock = bracket_ c_begin_draw c_end_draw
-}

drawing :: IO a -> IO a
drawing = bracket_ (c_begin_draw >> js_save) (js_restore >> c_end_draw)

-- Reset in case the transform stack gets messed up.
reset :: IO ()
reset = do
  js_resetTransform
  js_clearCanvas

-- Rendering

drawPath :: [(Double, Double)] -> Bool -> IO ()
drawPath [] _ = return ()
drawPath ((startX, startY):pts) close = do
  js_beginPath
  js_moveTo startX startY
  mapM_ (\(x, y) -> js_lineTo x y) pts
  if close
    then do
      js_closePath
      js_fill
    else
      js_stroke

renderPicture :: Picture -> IO ()
renderPicture Blank = return ()
renderPicture (Pictures ps) = mapM_ renderPicture ps

renderPicture (Translate x y p) = do
  js_save
  js_translate x y
  renderPicture p
  js_restore

renderPicture (Scale x y p) = do
  js_save
  js_scale x y
  renderPicture p
  js_restore

renderPicture (Rotate deg p) = do
  js_save
  let radians = -(deg * pi / 180.0)
  js_rotate radians
  renderPicture p
  js_restore

renderPicture (Color (RGBA r g b a) p) = do
  js_save
  js_setColor r g b a
  renderPicture p
  js_restore

renderPicture (Line pts) = drawPath pts False
renderPicture (Polygon pts) = drawPath pts True

renderPicture (Circle r) = do
  js_beginPath
  js_strokeCircle r

renderPicture (SolidCircle r) = do
  js_beginPath
  js_fillCircle r

renderPicture (Rectangle w h) = do
  js_beginPath
  js_strokeRect w h

renderPicture (SolidRectangle w h) = do
  js_beginPath
  js_fillRect w h

renderPicture (Text str) =
  withCAString str js_fillText

display :: Picture -> IO ()
display pic = do
  js_showCanvas
  drawing $ do
    js_clearCanvas
    js_setupCoordinates
    renderPicture pic

--------------------------------

animate :: (Double -> Picture) -> IO ()
animate frameFunc = do
  js_showCanvas
  startTime <- js_now

  let loop = do
        currentTime <- js_now
        let dt = (currentTime - startTime) / 1000.0

        drawing $ do
          js_clearCanvas
          js_setupCoordinates
          renderPicture (frameFunc dt)

        js_waitForFrame

        loop

  loop

---------------------------------

playWithSound :: world
     -> (world -> Picture)
     -> (world -> Sound)
     -> (Double -> MouseState -> world -> world)
     -> IO ()
playWithSound initialWorld drawFunc soundFunc stepFunc = do
  js_showCanvas
  startTime <- js_now

  let loop lastTime world = do
        currentTime <- js_now
        let dt = (currentTime - lastTime) / 1000.0

        -- Get the mouse billboard
        mState <- MouseState <$> js_mouseX <*> js_mouseY <*> ((/= 0) <$> js_mouseDown)

        let newWorld = stepFunc dt mState world

        drawing $ do
          js_clearCanvas
          js_setupCoordinates
          renderPicture (drawFunc newWorld)

        js_waitForFrame

        playSound (soundFunc newWorld)

        loop currentTime newWorld

  loop startTime initialWorld

play :: world
     -> (world -> Picture)
     -> (Double -> MouseState -> world -> world)
     -> IO ()
play initialWorld drawFunc stepFunc = playWithSound initialWorld drawFunc (const silence) stepFunc