identicon-0.2.3: Graphics/Identicon/Primitive.hs
-- |
-- Module : Graphics.Identicon.Primitive
-- Copyright : © 2016–present Mark Karpov
-- License : BSD 3 clause
--
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- Various primitives and combinators that help you write code for your
-- identicon. Filling functions is where you start. They create color layers
-- that occupy all available space. If you want to limit a layer in size,
-- specify where this smaller part should be, take a look at the “Position,
-- size, and shape” section. It also contains the 'circle' combinator that
-- limits a given filling is such a way that it forms a circle. Finally, we
-- have combinators that add symmetry to layers and other auxiliary
-- functions.
--
-- As a starting point, here is the function that generates a circle with
-- gradient filling changing from black (on the left hand side) to some
-- color (on the right hand side):
--
-- > f :: Word8 -> Word8 -> Word8 -> Layer
-- > f r g b = circle $ gradientLR id black (PixelRGB8 r g b)
--
-- The function consumes 3 bytes.
module Graphics.Identicon.Primitive
( -- * Filling
black,
color,
gradientLR,
gradientTB,
gradientTLBR,
gradientTRBL,
gradientXY,
-- ** Gradient transforming functions
-- $gtrans
mid,
edge,
-- * Position, size, and shape
onGrid,
circle,
-- * Symmetry
hsym,
vsym,
hvsym,
rsym,
-- * Other
oneof,
)
where
import Codec.Picture
import Data.Word (Word8)
import Graphics.Identicon
----------------------------------------------------------------------------
-- Filling
-- | Black is a special color, it means absence of light. We give this pixel
-- a name because it's used very frequently in layer coding.
black :: PixelRGB8
black = PixelRGB8 0 0 0
-- | Layer filled with a given color.
color :: PixelRGB8 -> Layer
color a = Layer $ \_ _ _ _ -> a
{-# INLINE color #-}
-- | Gradient changing from left to right.
gradientLR ::
-- | Gradient transforming function
(Float -> Float) ->
-- | Left color
PixelRGB8 ->
-- | Right color
PixelRGB8 ->
Layer
gradientLR f a b = Layer $ \w _ x _ ->
mixWith (const $ ξ f x w) a b
{-# INLINE gradientLR #-}
-- | Gradient changing from top to bottom.
gradientTB ::
-- | Gradient transforming function
(Float -> Float) ->
-- | Top color
PixelRGB8 ->
-- | Bottom color
PixelRGB8 ->
Layer
gradientTB f a b = Layer $ \_ h _ y ->
mixWith (const $ ξ f y h) a b
{-# INLINE gradientTB #-}
-- | Gradient changing from top left corner to bottom right corner.
gradientTLBR ::
-- | Gradient transforming function
(Float -> Float) ->
-- | Top left color
PixelRGB8 ->
-- | Bottom right color
PixelRGB8 ->
Layer
gradientTLBR f a b = Layer $ \w h x y ->
mixWith (const $ ξ f (x + y) (w + h)) a b
{-# INLINE gradientTLBR #-}
-- | Gradient changing from top right corner to bottom left corner.
gradientTRBL ::
-- | Gradient transforming function
(Float -> Float) ->
-- | Top right color
PixelRGB8 ->
-- | Bottom left color
PixelRGB8 ->
Layer
gradientTRBL f a b = Layer $ \w h x y ->
mixWith (const $ ξ f (w - x + y) (w + h)) a b
{-# INLINE gradientTRBL #-}
-- | Gradient with one color everywhere and another in the center.
gradientXY ::
-- | Gradient transforming function
(Float -> Float) ->
-- | “Edge” color
PixelRGB8 ->
-- | Color in the center
PixelRGB8 ->
Layer
gradientXY f a b = Layer $ \w h x y ->
let g x' y' = floor $ (1 - n) * fromIntegral x' + n * fromIntegral y'
n = f (nx * ny)
nx = mid (fromIntegral x / fromIntegral w)
ny = mid (fromIntegral y / fromIntegral h)
in mixWith (const g) a b
{-# INLINE gradientXY #-}
-- | A gradient helper function.
ξ ::
-- | Gradient transforming function
(Float -> Float) ->
-- | Actual value of coordinate
Int ->
-- | Maximum value of coordinate
Int ->
-- | Color at the beginning of the range
Word8 ->
-- | Color at the end of the range
Word8 ->
-- | Resulting color
Word8
ξ f v l x y = floor $ (1 - n) * fromIntegral x + n * fromIntegral y
where
n = f (fromIntegral v / fromIntegral l)
{-# INLINE ξ #-}
----------------------------------------------------------------------------
-- Gradient transforming functions
-- $gtrans
--
-- A note about “gradient transforming functions”: these normally map value
-- changing from 0 to 1 somehow, but they should not produce values outside
-- of that range. With help of such functions you can change character of
-- gradient transitions considerably.
-- | A built-in gradient transforming function. It maps continuous floating
-- value changing from 0 to 1 to value changing from 0 to 1 (in the middle)
-- and back to 0.
mid :: Float -> Float
mid x = 2 * (if x >= 0.5 then 1.0 - x else x)
{-# INLINE mid #-}
-- | This sharpens gradient transitions.
edge :: Float -> Float
edge x = x * x
{-# INLINE edge #-}
----------------------------------------------------------------------------
-- Position, size, and shape
-- | @onGrid w h n l@, given grid that has @w@ horizontal discrete positions
-- (of equal length) and @h@ vertical positions, it makes given layer @l@
-- occupy cell at index @n@. This approach allows you control position and
-- size at the same time.
--
-- The index @n@ can be greater than maximal index, in this case reminder of
-- division of @n@ by @w * h@ is used.
onGrid ::
(Integral a) =>
-- | Number of horizontal positions
Int ->
-- | Number of vertical positions
Int ->
-- | Index of this cell
a ->
-- | Layer to insert
Layer ->
-- | Resulting layer
Layer
onGrid α β n' l = Layer $ \w h x y ->
let n = fromIntegral n' `rem` (α * β)
(y', x') = n `quotRem` α
xu, yu :: Float
xu = fromIntegral w / fromIntegral α
yu = fromIntegral h / fromIntegral β
xA = floor (fromIntegral x' * xu)
xB = floor (fromIntegral (x' + 1) * xu)
yA = floor (fromIntegral y' * yu)
yB = floor (fromIntegral (y' + 1) * yu)
in if x < xA || x >= xB || y < yA || y >= yB
then black
else unLayer l (xB - xA) (yB - yA) (x - xA) (y - yA)
{-# INLINE onGrid #-}
-- | Limit given layer so that it forms a circle.
circle :: Layer -> Layer
circle l = Layer $ \w h x y ->
let w', h', v, r0, r1 :: Float
w' = fromIntegral w
h' = fromIntegral h
sqr a = a * a
v = sqr (fromIntegral x - w' / 2) + sqr (fromIntegral y - h' / 2)
r0 = min w' h' / 2
r1 = sqr r0
β = 2.0 * r0
δ = (r1 - v) / β
τ = floor . (* δ) . fromIntegral
~px@(PixelRGB8 r g b) = unLayer l w h x y
e
| v < r1 - β = px
| v <= r1 = PixelRGB8 (τ r) (τ g) (τ b)
| otherwise = black
in e
{-# INLINE circle #-}
----------------------------------------------------------------------------
-- Symmetry
-- | Add horizontal symmetry to a layer.
hsym :: Layer -> Layer
hsym l = Layer $ \w h x y ->
let w' = w `quot` 2
in unLayer l w' h (if x > w' then w - x else x) y
{-# INLINE hsym #-}
-- | Add vertical symmetry to a layer.
vsym :: Layer -> Layer
vsym l = Layer $ \w h x y ->
let h' = h `quot` 2
in unLayer l w h' x (if y > h' then h - y else y)
{-# INLINE vsym #-}
-- | Add horizontal and vertical symmetry to layer. Result is a layer with
-- four mirrored repetitions of the same figure.
hvsym :: Layer -> Layer
hvsym l = Layer $ \w h x y ->
let h' = h `quot` 2
w' = w `quot` 2
in unLayer
l
w'
h'
(if x > w' then w - x else x)
(if y > h' then h - y else y)
{-# INLINE hvsym #-}
-- | Just like 'hvsym', but every repetition is rotated by 90°. Only works
-- with square layers because for speed it just swaps coordinates.
rsym :: Layer -> Layer
rsym l = Layer $ \w h x y ->
let h' = h `quot` 2
w' = w `quot` 2
α = x > w'
β = y > h'
in unLayer
l
w'
h'
(if α then (if β then w - x else y) else (if β then h - y else x))
(if β then (if α then h - y else x) else (if α then w - x else y))
{-# INLINE rsym #-}
----------------------------------------------------------------------------
-- Other
-- | Select one of the provided alternatives given a number.
oneof :: (Integral n) => [a] -> n -> a
oneof xs n = xs !! (fromIntegral n `rem` length xs)
{-# INLINE oneof #-}