packages feed

keid-render-basic-0.1.6.1: src/Resource/Font/EvanW.hs

{- |
  JSON font loader for bitmaps and SDFs

  Generator: https://evanw.github.io/font-texture-generator/

  Usage (WebGL): https://evanw.github.io/font-texture-generator/example-webgl/
-}

module Resource.Font.EvanW
  ( load
  , Container(..)
  , Character(..)

  , putLine
  , PutChar(..)
  ) where

import RIO

import Data.Aeson (FromJSON, eitherDecodeStrict')
import Foreign qualified
import Geomancy (Vec2, vec2, pattern WithVec2)
import GHC.Stack (withFrozenCallStack)
import RIO.HashMap qualified as HashMap
import RIO.Text qualified as Text
import Vulkan.NamedType ((:::))

import Engine.UI.Layout qualified as Layout
import Resource.Source (Source)
import Resource.Source qualified as Source

-- * Loading

newtype FontError = FontError Text
  deriving (Eq, Ord, Show, Generic)

instance Exception FontError

data Container = Container
  { name       :: Text
  , size       :: Float
  , bold       :: Bool
  , italic     :: Bool
  , width      :: Float
  , height     :: Float
  , characters :: HashMap Char Character
  }
  deriving (Eq, Ord, Show, Generic)

data Character = Character
  { x       :: Float
  , y       :: Float
  , width   :: Float
  , height  :: Float
  , originX :: Float
  , originY :: Float
  , advance :: Float
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Container
instance FromJSON Character

load
  :: ( MonadIO m
     , MonadReader env m
     , HasLogFunc env
     , HasCallStack
     )
  => Source -> m Container
load =
  withFrozenCallStack $
    Source.load \bytes ->
      case eitherDecodeStrict' bytes of
        Left err ->
          liftIO . throwIO . FontError $ Text.pack err
        Right res ->
          pure res

-- * Typesetting

data PutChar = PutChar
  { pcPos    :: Vec2
  , pcSize   :: Vec2
  , pcOffset :: Vec2
  , pcScale  :: Vec2
  } deriving (Show)

instance Foreign.Storable PutChar where
  alignment ~_ = 16

  sizeOf ~_ = 32 -- 4 of pairs of floats

  peek ptr = PutChar
    <$> Foreign.peekElemOff (Foreign.castPtr ptr) 0
    <*> Foreign.peekElemOff (Foreign.castPtr ptr) 1
    <*> Foreign.peekElemOff (Foreign.castPtr ptr) 2
    <*> Foreign.peekElemOff (Foreign.castPtr ptr) 3

  poke ptr PutChar{..} = do
    Foreign.pokeElemOff (Foreign.castPtr ptr) 0 pcPos
    Foreign.pokeElemOff (Foreign.castPtr ptr) 1 pcSize
    Foreign.pokeElemOff (Foreign.castPtr ptr) 2 pcOffset
    Foreign.pokeElemOff (Foreign.castPtr ptr) 3 pcScale

putLine
  :: "WH"        ::: Vec2
  -> "XY"        ::: Vec2
  -> "Alignment" ::: Layout.Alignment
  -> "Size"      ::: Float
  -> "Font"      ::: Container
  -> "Line"      ::: [Char]
  -> ("scale" ::: Float, [PutChar])
putLine (WithVec2 cw ch) (WithVec2 cx cy) Layout.Alignment{..} targetSize font =
  (sizeScale,) . extract . foldl' step (0, 0, [])
  where
    Container
      { size   = fontSize
      , width  = atlasWidth
      , height = atlasHeight
      , characters
      } = font

    sizeScale = targetSize / fontSize

    extract (offX, _offY, bits) = do
      (WithVec2 w h, WithVec2 x y, (offset, scale)) <- bits
      let
        ax = case alignX of
          Layout.Begin  -> -cw / 2
          Layout.Middle -> -offX * sizeScale / 2
          Layout.End    -> cw / 2 - offX * sizeScale

        ay = case alignY of
          Layout.Begin  -> -ch / 2 + targetSize * 1.3
          Layout.Middle -> targetSize * 0.5
          Layout.End    -> ch / 2 - targetSize * 0.5

      pure PutChar
        { pcPos    = vec2 (cx + ax + x * sizeScale) (cy + ay + y * sizeScale)
        , pcSize   = vec2 (w * sizeScale) (h * sizeScale)
        , pcOffset = offset
        , pcScale  = scale
        }

    step (offX, offY, acc) ' ' =
      ( offX + fontSize / 2
      , offY
      , acc
      )

    step (offX, offY, acc) char =
      case HashMap.lookup char characters <|> HashMap.lookup '?' characters of
        Nothing ->
          (offX, offY, acc)
        Just Character{..} ->
          ( offX + advance
          , offY
          , ( vec2 width (-height)
            , vec2 ox oy
            , (uvOffset, uvScale)
            ) : acc
          )
          where
            ox = offX + width / 2 - originX
            oy = offY + height / 2 - originY

            uvOffset = vec2 (x / atlasWidth) (y / atlasHeight)
            uvScale  = vec2 (width / atlasWidth) (height / atlasHeight)