Выводить ascii-анимацию из Haskell?

Я пытаюсь получить очень быстрое и грязное анимированное отображение некоторых данных, созданных с помощью Haskell. Самое простое, что можно попробовать, это ASCII-графика — другими словами, что-то вроде:

type Frame = [[Char]]     -- each frame is given as an array of characters

type Animation = [Frame]

display :: Animation -> IO ()
display = ??

Как мне лучше всего это сделать?

Часть, которую я вообще не могу понять, это как обеспечить минимальную паузу между кадрами; остальное просто, используя putStrLn вместе с clearScreen из ansi-terminal, найденный через этот ответ.


person PLL    schedule 16.09.2011    source источник
comment
если вы очистите экран, он будет мерцать. есть фрактальный зумер ascii для haskell, проверьте его.   -  person Karoly Horvath    schedule 17.09.2011


Ответы (2)


Ну, вот грубый набросок того, что я буду делать:

import Graphics.UI.SDL.Time (getTicks)
import Control.Concurrent (threadDelay)

type Frame = [[Char]]
type Animation = [Frame]

displayFrame :: Frame -> IO ()
displayFrame = mapM_ putStrLn

timeAction :: IO () -> IO Integer
timeAction act = do t <- getTicks
                    act
                    t' <- getTicks
                    return (fromIntegral $ t' - t)

addDelay :: Integer -> IO () -> IO ()
addDelay hz act = do dt <- timeAction act
                     let delay = calcDelay dt hz
                     threadDelay $ fromInteger delay

calcDelay dt hz = max (frame_usec - dt_usec) 0
  where frame_usec = 1000000 `div` hz
        dt_usec = dt * 1000

runFrames :: Integer -> Animation -> IO ()
runFrames hz frs = mapM_ (addDelay hz . displayFrame) frs

Очевидно, что здесь я использую SDL исключительно для getTicks, потому что это то, что я использовал раньше. Не стесняйтесь заменить его любой другой функцией, чтобы получить текущее время.

Первый аргумент runFrames — это, как следует из названия, частота кадров в герцах, т. е. кадров в секунду. Функция runFrames сначала преобразует каждый кадр в действие, которое его отрисовывает, затем отдает каждый кадр функции addDelay, которая проверяет время до и после запуска действия, а затем приостанавливается, пока время кадра не истечет.

Мой собственный код выглядел бы немного иначе, чем этот, потому что у меня обычно был бы более сложный цикл, который делал бы другие вещи, например, опрос SDL для событий, выполнение фоновой обработки, передачу данных на следующую итерацию и т. д. Но основная идея та же.

Очевидно, преимуществом этого подхода является то, что, несмотря на простоту, вы получаете стабильную частоту кадров, когда это возможно, с четкими средствами указания целевой скорости.

person C. A. McCann    schedule 16.09.2011
comment
Большое спасибо! В конце концов, я использовал по существу это, но заменил getTicks на getClockTime из System.Time, чтобы избежать загрузки дополнительных пакетов. (Haskell — ленивый язык, верно? :-)) - person PLL; 21.09.2011

Это основано на ответе CA McCann, который хорошо работает, но не является стабильным во времени в долгосрочной перспективе, в частности, когда частота кадров не является целой долей частоты тиков.

import GHC.Word (Word32)

-- import CAMcCann'sAnswer (Frame, Animation, displayFrame, getTicks, threadDelay)

atTick :: IO () -> Word32 -> IO ()
act `atTick` t = do
    t' <- getTicks
    let delay = max (1000 * (t-t')) 0
    threadDelay $ fromIntegral delay
    act

runFrames :: Integer -> Animation -> IO ()
runFrames fRate frs = do
    t0 <- getTicks
    mapM_ (\(t,f) -> displayFrame f `atTick` t) $ timecode fRate32 t0 frs
  where timecode ν t0 = zip [ t0 + (1000 * i) `div` ν | i <- [0..] ]
        fRate32 = fromIntegral fRate :: Word32
person leftaroundabout    schedule 18.09.2011
comment
Ах, здорово! В большей части моего фактического кода у меня была параметрическая анимация, основанная на разнице во времени, и, таким образом, я не слишком беспокоился о точной частоте кадров, поэтому я не думал делать такие вещи в верхней части. моя голова. Тем не менее, это определенно путь для дискретных кадров. - person C. A. McCann; 20.09.2011