-- | -- Module : Graphics.Avatars.Pixelated -- Description : Contains types and functions for generating pixelated avatars. -- Copyright : (c) Christopher Wells, 2016 -- License : MIT -- Maintainer : cwellsny@nycap.rr.com -- -- This module provides types and functions for creating and working with -- pixelated avatars. -- -- Avatars can be generated by providing a `Seed`. Seeds can be created by -- passing a random String into `createSeed`. The given String is then turned -- into an MD5 checksum, which is used as the seed value. -- -- Once a Seed has been created, an Avatar can be generated from it by passing -- it into `generateAvatar`. By default, Avatars start at a size of 8x8px. The -- size of an Avatar can be increased by passing it into `scaleAvatar`. Once -- you have scaled the Avatar to the size you want it to be it can then be -- saved to a file by passing it into `saveAvatar`. -- -- By default, the `saveAvatar` function saves the avatar as a png image file, -- however you can also save avatars in other image formats by using -- `saveAvatarWith`. It allows you to specify the function to use to convert -- the avatar image into an image ByteString. -- -- = Example -- The following is an example showing how to construct a function which will -- generate a 256x256px avatar from a given seed string, and save it at the -- given location. -- -- @ -- import Graphics.Avatars.Pixelated -- -- createAndSaveAvatar :: String -> FilePath -> IO () -- createAndSaveAvatar s path = saveAvatar avatar path -- where avatar = scaleAvatar 32 $ generateAvatar seed -- seed = createSeed s -- @ module Graphics.Avatars.Pixelated ( -- * Types -- ** Seed Seed(..), createSeed, -- ** Avatar Avatar(..), generateAvatar, scaleAvatar, saveAvatar, saveAvatarWith, convertAvatarToImage, -- *** Image Conversion ImageConversion, encodeToPng, encodeToGif, encodeToTiff, -- ** Color Color(..), getColorValue, colorFromSeed, -- ** Avatar Grid AvatarGrid(..), showGrid, generateAvatarGrid, -- ** Utility scaleList ) where import Codec.Picture (encodeColorReducedGifImage, encodePng, encodeTiff, generateImage, Image(..), PixelRGB8(..)) import Data.Char (ord) import qualified Data.ByteString.Lazy as B (ByteString, writeFile) import Data.ByteString.Lazy.Internal (packChars) import Data.Digest.Pure.MD5 (md5) import Data.List.Split (chunksOf) ------------------------------------------------------------------------------- -- Seeds -- | A seed to use in generating an avatar. Can be created from a String by -- using the `createSeed` function. -- -- Seeds are expected to be 32 character hexidecimal MD5 checksums. newtype Seed = Seed { unSeed :: String } deriving (Eq, Show) -- | Creates a seed from a given String. -- -- >>> createSeed "Hello" -- Seed {unSeed = "8b1a9953c4611296a827abf8c47804d7"} createSeed :: String -> Seed createSeed = Seed . show . md5 . packChars ------------------------------------------------------------------------------- -- Avatars -- | A generated avatar, comprised of a color and a grid representing the -- visual pattern of the avatar image. Can be created from a seed using -- `generateAvatar`. -- -- Avatars are generated at a size of 8x8px, and can be scaled up to larger -- image sizes by using the `scaleAvatar` function. data Avatar = Avatar { color :: Color , grid :: AvatarGrid } deriving (Eq) -- | Generates a String containing the color and pattern of the avatar. instance Show Avatar where show a = (show . color) a ++ "\n" ++ ((show . grid) a) -- | Generates an avatar from the given seed. -- -- >>> generateAvatar Seed {unSeed = "8b1a9953c4611296a827abf8c47804d7"} -- Grey -- ██ ██ ██ -- ██ ██ -- █ █ -- █ █ -- ██ ██ -- ████████ -- █ ██ █ -- █ █ generateAvatar :: Seed -> Avatar generateAvatar seed = avatar where avatar = Avatar { color = aColor , grid = aGrid } aColor = colorFromSeed seed aGrid = generateAvatarGrid seed -- | Scales the given Avatar by the given scaling factor. -- -- For example, scaling an 8x8px avatar by a factor of 4 would transform it -- into a 32x32px avatar. scaleAvatar :: Int -> Avatar -> Avatar scaleAvatar factor avatar = avatar { grid = AvatarGrid scaledGrid } where scaledGrid = ((scaleList factor) . (map (scaleList factor))) unscaledGrid unscaledGrid = unAvatarGrid $ grid avatar -- | Saves the given avatar as a png image file to the given file path. The -- filepath should be the path and name of the image file to be created, -- including the file extension. -- -- For saving an avatar in a non-png format, use `saveAvatarWith`. -- -- @ -- makeAvatar :: Seed -> FilePath -> IO () -- makeAvatar seed path = do -- let avatar = generateAvatar seed path -- saveAvatar avatar path -- @ saveAvatar :: Avatar -> FilePath -> IO () saveAvatar = saveAvatarWith encodeToPng -- | Saves the given avatar to the given file location, using the given -- function to encode it into a specific image format. -- -- Some examples of encoding functions are `encodeToGif` and `encodeToTiff`. -- -- @ -- saveTiffAvatar :: Seed -> FilePath -> IO () -- saveTiffAvatar seed path = do -- let avatar = generateAvatar seed path -- saveAvatarWith encodeTiff avatar path -- @ saveAvatarWith :: ImageConversion -> Avatar -> FilePath -> IO () saveAvatarWith conversion avatar path = do let image = conversion $ convertAvatarToImage avatar B.writeFile path image -- | Converts the given Avatar into an Image. convertAvatarToImage :: Avatar -> Image PixelRGB8 convertAvatarToImage avatar = image where image = generateImage getPixel dimension dimension dimension = length colorGrid getPixel x y = colorGrid !! y !! x colorGrid = (map . map) (toPixel $ color avatar) $ unAvatarGrid $ grid avatar toPixel c v = if v then getColorValue c else PixelRGB8 255 255 255 ---------------------------------------- -- Image Conversion -- | A function which converts an image into a lazy ByteString. type ImageConversion = (Image PixelRGB8 -> B.ByteString) -- | Converts an image into a Png image ByteString. encodeToPng :: ImageConversion encodeToPng = encodePng -- | Converts an image into a Gif image ByteString. encodeToGif :: ImageConversion encodeToGif img = case encodeColorReducedGifImage img of Right i -> i Left _ -> error "Unable to create valid gif color palette for avatar image." -- | Converts an image into a Tiff image ByteString. encodeToTiff :: ImageConversion encodeToTiff = encodeTiff ------------------------------------------------------------------------------- -- Colors -- | A color of an avatar. The color of an avatar is the color that is applied -- to the pattern of the avatar when it is converted into an image. data Color = Black | Blue | Green | Grey | Orange | Purple | Red | Yellow deriving (Eq, Show, Enum) -- | Converts the given color into a RGB pixel representation. -- -- >>> getColorValue Orange -- PixelRGB8 255 140 65 getColorValue :: Color -> PixelRGB8 getColorValue c | c == Black = PixelRGB8 41 41 41 | c == Blue = PixelRGB8 104 182 255 | c == Green = PixelRGB8 114 220 131 | c == Grey = PixelRGB8 150 150 150 | c == Orange = PixelRGB8 255 140 65 | c == Purple = PixelRGB8 208 148 255 | c == Red = PixelRGB8 255 87 87 | otherwise = PixelRGB8 255 231 148 -- | Picks an avatar color using the given seed. -- -- Each of the eight possible colors has roughly an equal chance of being -- chosen with a random seed. -- -- >>> colorFromSeed $ Seed {unSeed = "8b1a9953c4611296a827abf8c47804d7"} -- Grey colorFromSeed :: Seed -> Color colorFromSeed = genColor . dSum . unSeed where twoDigits n = map ord $ take 2 n dSum n = foldr (+) 1 $ twoDigits n genColor a = [Black .. Yellow] !! (a `mod` 8) ------------------------------------------------------------------------------- -- AvatarGrids -- | A grid of boolean values representing an Avatar. True values indicate -- colored pixels, and False values indicate blank pixels. newtype AvatarGrid = AvatarGrid { unAvatarGrid :: [[Bool]] } deriving (Eq) -- | Converts the grid into a String representation. instance Show AvatarGrid where show x = (showGrid . unAvatarGrid) x -- | The left half of an AvatarGrid. newtype AvatarGridSide = AvatarGridSide { unAvatarGridSide :: [[Bool]] } -- | Converts the grid side into a String representation. instance Show AvatarGridSide where show x = (showGrid . unAvatarGridSide) x -- | Converts a grid of boolean values into a String representation. -- -- >>> putStrLn $ showGrid [[True, False], [False, True]] -- █ -- █ showGrid :: [[Bool]] -> String showGrid g = (init . unlines) $ (map . map) showPixel g -- | Converts a boolean value into a character representation for a pixel. -- -- >>> showPixel True -- '\9608' -- >>> showPixel False -- ' ' showPixel :: Bool -> Char showPixel p = if p then '█' else ' ' -- | Generates an AvatarGrid using the given Seed. -- -- It works by generating the left half of the grid using the contents of the -- Seed, and then mirroring the left half to create the full grid. -- -- >>> generateAvatarGrid Seed {unSeed = "8b1a9953c4611296a827abf8c47804d7"} -- ██ ██ ██ -- ██ ██ -- █ █ -- █ █ -- ██ ██ -- ████████ -- █ ██ █ -- █ █ generateAvatarGrid :: Seed -> AvatarGrid generateAvatarGrid = mirrorGrid . generateAvatarGridSide -- | Creates a full AvatarGrid by mirroring the given AvatarGridSide on the -- y-axis. mirrorGrid :: AvatarGridSide -> AvatarGrid mirrorGrid side = AvatarGrid $ map mirror $ unAvatarGridSide side where mirror l = l ++ (reverse l) -- | Generates the right side of an AvatarGrid using the given seed. generateAvatarGridSide :: Seed -> AvatarGridSide generateAvatarGridSide = AvatarGridSide . numToGrid . unSeed -- | Converts the given hexidecimal number String into a grid of boolean values. numToGrid :: String -> [[Bool]] numToGrid s = boolGrid where boolGrid = (map . map) convertToPixel $ (map . map) ord numGrid numGrid = chunksOf 4 s convertToPixel = (> ord '7') ------------------------------------------------------------------------------- -- Utilities -- | Scales the given list by the given scaling factor. -- -- >>> scaleList 2 [1, 2] -- [1,1,2,2] -- >>> scaleList 3 [0, 1] -- [0,0,0,1,1,1] scaleList :: Int -> [a] -> [a] scaleList _ [] = [] scaleList factor (x:xs) = replicate factor x ++ scaleList factor xs