packages feed

keid-render-basic-0.1.9.0: 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 Geomancy.Layout qualified as Layout
import Geomancy.Layout.Alignment (Alignment)
import Geomancy.Layout.Box (Box(..))
import Geomancy.Layout.Box qualified as Box
import GHC.Stack (withFrozenCallStack)
import RIO.HashMap qualified as HashMap
import RIO.Text qualified as Text
import Vulkan.NamedType ((:::))

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" ::: Alignment
  -> "Size"      ::: Float
  -> "Font"      ::: Container
  -> "Line"      ::: [Char]
  -> ("scale" ::: Float, [PutChar])
putLine bs bp a targetSize font =
  (sizeScale,) . extract . foldl' step (0, [])
  where
    parent = Box
      { position = bp
      , size = bs
      }

    Container
      { size   = fontSize
      , width  = atlasWidth
      , height = atlasHeight
      , characters
      } = font

    sizeScale = targetSize / fontSize
    baseline = 0.7125

    extract (offX, bits) =
      Box.withTRBL (Layout.placeSize a (vec2 (offX * sizeScale) targetSize) parent) \t _r _b l -> do
        (WithVec2 w h, WithVec2 x y, (pcOffset, pcScale)) <- bits
        let
          pcPos =
            vec2
              (l + x * sizeScale)
              (t + y * sizeScale + baseline * targetSize)

          pcSize =
            vec2
              (w * sizeScale)
              (h * sizeScale)

        pure PutChar{..}

    step (offX, acc) = \case
      ' ' ->
        ( offX + fontSize / 2
        , acc
        )

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

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