packages feed

ktx-font-0.1.0.0: src/Codec/Ktx2/Font/Shaping.hs

module Codec.Ktx2.Font.Shaping
  ( -- * Input
    shapeText
  , shape
  , TextShape.text_
  , TextShape.char_
  , withFont_
  , shapeWith
  , Cursor(..)
  , initialCursorUp
  , initialCursorDown
    -- * Output
  , PlacedRun
  , PlacedGlyph(..)
  -- * Re-exports
  , KBTS.Font
  , Atlas.Compact(..)
  , Atlas.Box(..)
  ) where

import Codec.Ktx2.Font qualified as Font
import Control.Concurrent (withMVar)
import Data.List (mapAccumL)
import Data.Text (Text)
import Data.Text qualified as Text
import Graphics.MSDF.Atlas.Compact qualified as Atlas
import KB.Text.Shape qualified as TextShape
import KB.Text.Shape.FFI.Handles qualified as Handles
import KB.Text.Shape.Font qualified as KBTS

{- | Perform text segmentation and shaping on a block of text
-}
shapeText :: Cursor -> Font.StackContext a -> Text -> IO [PlacedRun]
shapeText cur ctx t =
  if Text.null t then
    pure []
  else
    shape cur ctx (TextShape.text_ t)

{- | Perform text segmentation and shaping.

Feed data using functions like 'TextShape.text_'.

The next step would be converting the resulting glyph "runs" into API specific data.

The atlas texture coordinates are normalized to 0..1 range.

The glyph "model" coordinates are normallized to fonts' "capital height".
Multiply by font size to match your projection settings.
-}
shape
  :: Cursor
  -> Font.StackContext a
  -> ((?shapeContext :: Handles.ShapeContext) => IO ())
  -> IO [PlacedRun]
shape cur ctx action  = snd <$> shapeWith (collectRun ctx) cur ctx action

{- | Run shaping and process results using a custom accumulator function
-}
shapeWith
  :: (acc -> (TextShape.Run, [TextShape.Glyph]) -> (acc, placed)) -- ^ Results-collecting function
  -> acc -- initial accumulator
  -> Font.StackContext a -- ^ shaping context with
  -> ((?shapeContext :: Handles.ShapeContext) => IO ())
  -> IO (acc, [placed])
shapeWith collectFun cur Font.StackContext{shapeContext} action =
  withMVar shapeContext \kbts ->
    mapAccumL collectFun cur <$> TextShape.run kbts action

withFont_ :: (?shapeContext :: Handles.ShapeContext) => Font.Bundle -> IO () -> IO ()
withFont_ Font.Bundle{fontData} action =
  KBTS.withFontData fontData \font ->
    TextShape.withFont_ font action

data Cursor = Cursor
  { curX, curY :: Float
  , lineHeight :: Float -- ^ Space between the baselines, as a multiple of the font size.
  , ySign :: Float -- ^ Should match atlas yOrigin and screen direction.
  }
  deriving (Eq, Show)

initialCursorUp
  :: Float -- ^ Line height multiplier.
  -> Cursor
initialCursorUp = initialCursor (-1)

initialCursorDown
  :: Float -- ^ Line height multiplier.
  -> Cursor
initialCursorDown = initialCursor 1

initialCursor
  :: Float -- ^ Y axis signum
  -> Float -- ^ Line height multiplier.
  -> Cursor
initialCursor ySign lineHeight = Cursor
  { curX = 0
  , curY = 0
  , lineHeight
  , ySign
  }

-- | Text runs with uniform direction and script.
type PlacedRun =
  ( (KBTS.Font, Maybe Atlas.Compact)
  , [PlacedGlyph]
  )

data PlacedGlyph = PlacedGlyph
  { codepoint :: Char -- ^ Unicode codepoint associated with the glyph. Mostly for debugging.
  , glyphId :: Int -- ^ Glyph ID in font and atlas. You can use this to look up the glyph boxes from GPU if you upload the glyph data as arrays.
  , glyph :: Atlas.Box -- ^ Glyph box on screen. The size and offsets are normalized so you can run the shaping once, then transform the whole block as you need.
  , plane :: Atlas.Box -- ^ Glyph box in atlas. The size is normalized to UV of the texure.
  }
  deriving (Eq, Show)

collectRun :: Font.StackContext a -> Cursor -> (TextShape.Run, [TextShape.Glyph]) -> (Cursor, PlacedRun)
collectRun ctx cur (TextShape.Run{font}, glyphs) = placeRun <$> mapAccumL (place fontUnitScale atlas_) cur glyphs
  where
    fontUnitScale = capHeightScale font
    atlas_ = Font.lookupAtlas font ctx

    placeRun :: [(Char, Int, Maybe (Atlas.Box, Atlas.Box))] -> PlacedRun
    placeRun placed =
      ( (font, atlas_)
      , do
          (codepoint, glyphId, Just (glyph, plane)) <- placed
          pure PlacedGlyph{codepoint, glyphId, glyph, plane}
      )

-- | Normalize font metrics using "cap height"
capHeightScale :: KBTS.Font -> Float
capHeightScale font = case KBTS.capHeight font of
 0 -> error "capHeight not set"
 n -> 1 / n

-- -- | Normalize font metrics using "units per em"
-- emScale :: KBTS.Font -> Float
-- emScale font = 1 / KBTS.unitsPerEm font

place :: Float -> Maybe Atlas.Compact -> Cursor -> TextShape.Glyph -> (Cursor, (Char, Int, Maybe (Atlas.Box, Atlas.Box)))
place fontUnitScale atlas_ cur@Cursor{..} glyph@TextShape.Glyph{codepoint, offsetX, offsetY, id=glyphId} =
  ( nextCur
  , (codepoint, fromIntegral glyphId, if codepoint == '\n' then Nothing else params_)
  )
  where
    nextCur = advance fontUnitScale cur glyph

    params_ = atlas_ >>= Atlas.lookupGlyph (fromIntegral glyphId) >>= pure . fmap placeGlyph
    placeGlyph =
      Atlas.moveBox
        (curX + fromIntegral offsetX * fontUnitScale)
        (curY + fromIntegral offsetY * fontUnitScale * ySign)

advance :: Float -> Cursor -> TextShape.Glyph -> Cursor
advance fontUnitScale cur@Cursor{..} TextShape.Glyph{advanceX, advanceY, codepoint} =
  if codepoint == '\n' then
    cur
      { curX = 0
      , curY = curY + lineHeight * ySign
      }
  else
    cur
      { curX = curX + fromIntegral advanceX * fontUnitScale
      , curY = curY + fromIntegral advanceY * fontUnitScale * ySign
      }