-- |
-- 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.Digest.Pure.MD5 (md5)
import Data.List.Split (chunksOf)
import Data.String (fromString)
-------------------------------------------------------------------------------
-- 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 . fromString
-------------------------------------------------------------------------------
-- 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 = showGrid . unAvatarGrid
-- | The left half of an AvatarGrid.
newtype AvatarGridSide = AvatarGridSide { unAvatarGridSide :: [[Bool]] }
-- | Converts the grid side into a String representation.
instance Show AvatarGridSide where
show = showGrid . unAvatarGridSide
-- | 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 . 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