piet-0.1: sources/Language/Piet/Types.hs
{-# LANGUAGE CPP #-}
-- | A module providing a couple of Piet-specific types
-- and simple associated functions needed throughout the library.
module Language.Piet.Types
(
-- * Piet Interpreter
-- ** Direction Pointer and Codel Chooser
DirectionPointer(..), addCoordinates, rotate
, CodelChooser(..), toggle
-- ** Piet's type system
, PietType(..)
-- ** Runtime program representation
, Program(..), isBlocked
-- * Colour system
, Colour(..), rgba2Colour, rgb2Colour
, HueColour(..), hueChange
, Lightness(..), lightnessChange
-- * Images
, Image, imgWidth, imgHeight, imgInRange, imgNew, imgPixel, imgSetPixel
, LabelKey, LabelInfo(..), labelSize, addPixel, LabelBorder(..)
) where
import Data.Array.IArray
import Data.IntMap hiding ((!))
import Data.Monoid
import Data.Ord
-- | The Direction Pointer (DP).
data DirectionPointer
= DPRight
| DPDown
| DPLeft
| DPUp
deriving (Show, Read, Eq, Ord, Enum)
-- | Move coordinates by one in the direction of the 'DirectionPointer'.
addCoordinates :: DirectionPointer -- ^ Direction to move to
-> Int -- ^ x-coordinate
-> Int -- ^ y-coordinate
-> (Int, Int) -- ^ New x-/y-coordinates
addCoordinates DPRight x y = (x + 1, y)
addCoordinates DPDown x y = (x, y + 1)
addCoordinates DPLeft x y = (x - 1, y)
addCoordinates DPUp x y = (x, y - 1)
-- | Rotate a 'DirectionPointer' clockwise (counter clockwise if the 'Int' is
-- negative) a given number of times.
rotate :: Int -> DirectionPointer -> DirectionPointer
rotate n dp = let n' = (n + fromEnum dp) `rem` 4
in toEnum $ if n' < 0 then n' + 4 else n'
-- | The Codel Chooser (CC).
data CodelChooser
= CCLeft
| CCRight
deriving (Show, Read, Eq, Ord, Enum)
-- | Toggle a 'CodelChooser' a given number of times.
toggle :: Int -> CodelChooser -> CodelChooser
toggle n cc = let n' = (n + fromEnum cc) `rem` 2
in toEnum $ if n' < 0 then n' + 2 else n'
-- | Piet types. Relevant to distinguish in-/output strategies.
data PietType
= PietNumber
| PietChar
deriving (Show, Read, Eq, Ord)
-- | Runtime program representation.
data Program = Program
{ image :: Image Colour -- ^ Original image
, mask :: Image LabelKey -- ^ Labelled image
, info :: IntMap LabelInfo -- ^ Information about the labels
}
-- | Returns if a given codel in a program is blocked in the Piet
-- sense (which is the case when it is out of the image's range or
-- 'Black').
isBlocked :: Int -> Int -> Program -> Bool
isBlocked x y program = (not (imgInRange x y (image program)))
|| (Black == imgPixel x y (image program))
-- | The colours that make up a Piet program text.
data Colour
= Black
| White
| Hue {-# UNPACK #-} !Lightness {-# UNPACK #-} !HueColour
deriving (Show, Read, Eq, Ord)
-- | Converts red\/green\/blue\/alpha values to a 'Colour'. The alpha channel
-- is ignored for now, but may be used in future implementations or
-- dialects, so please use this function instead of 'rgb2Colour' whenever
-- an alpha channel is available.
rgba2Colour :: Num w => w -- ^ red
-> w -- ^ green
-> w -- ^ blue
-> w -- ^ alpha
-> Colour
rgba2Colour r g b _ = rgb2Colour r g b
-- | Converts red\/green\/blue values to a 'Colour'. If the supplied
-- arguments do not form a proper Piet 'Colour', 'White' is returned.
rgb2Colour :: Num w => w -- ^ red
-> w -- ^ green
-> w -- ^ blue
-> Colour
rgb2Colour 0x00 0x00 0x00 = Black
rgb2Colour 0xff 0xff 0xff = White
rgb2Colour 0xff 0xc0 0xc0 = Hue Light Red
rgb2Colour 0xff 0x00 0x00 = Hue Normal Red
rgb2Colour 0xc0 0x00 0x00 = Hue Dark Red
rgb2Colour 0xff 0xff 0xc0 = Hue Light Yellow
rgb2Colour 0xff 0xff 0x00 = Hue Normal Yellow
rgb2Colour 0xc0 0xc0 0x00 = Hue Dark Yellow
rgb2Colour 0xc0 0xff 0xc0 = Hue Light Green
rgb2Colour 0x00 0xff 0x00 = Hue Normal Green
rgb2Colour 0x00 0xc0 0x00 = Hue Dark Green
rgb2Colour 0xc0 0xff 0xff = Hue Light Cyan
rgb2Colour 0x00 0xff 0xff = Hue Normal Cyan
rgb2Colour 0x00 0xc0 0xc0 = Hue Dark Cyan
rgb2Colour 0xc0 0xc0 0xff = Hue Light Blue
rgb2Colour 0x00 0x00 0xff = Hue Normal Blue
rgb2Colour 0x00 0x00 0xc0 = Hue Dark Blue
rgb2Colour 0xff 0xc0 0xff = Hue Light Magenta
rgb2Colour 0xff 0x00 0xff = Hue Normal Magenta
rgb2Colour 0xc0 0x00 0xc0 = Hue Dark Magenta
rgb2Colour _ _ _ = White
-- | Piet colours in the hue cycle.
data HueColour
= Red
| Yellow
| Green
| Cyan
| Blue
| Magenta
deriving (Show, Read, Eq, Ord, Enum)
-- | Hue difference between two 'HueColour's. 'Red' means no change,
-- 'Yellow' one step and so forth.
hueChange :: HueColour -> HueColour -> HueColour
hueChange c1 c2 = toEnum $ (fromEnum c2 - fromEnum c1) `mod` 6
-- | Hue lightness values supported by Piet.
data Lightness
= Light
| Normal
| Dark
deriving (Show, Read, Eq, Ord, Enum)
-- | Lightness difference between Piet lightness values. 'Light'
-- represents no change, 'Normal' one step darker and 'Dark'
-- two steps darker.
lightnessChange :: Lightness -> Lightness -> Lightness
lightnessChange l1 l2 = toEnum $ (fromEnum l2 - fromEnum l1) `mod` 3
-- | An image. Its coordinates will be @(0, 0) .. (width-1, height-1)@
data Image a = Image
{ imgWidth :: {-# UNPACK #-} !Int -- ^ Width of an 'Image' in pixels.
, imgHeight :: {-# UNPACK #-} !Int -- ^ Height of an 'Image' in pixels.
, imgPixels :: !(Array (Int, Int) a) -- ^ An 'Array' storing the pixels of an 'Image'.
}
#ifndef __HADDOCK__
deriving (Show, Eq, Ord)
#else
instance (Show a) => Show (Image a)
instance (Eq a) => Eq (Image a)
instance (Ord a) => Ord (Image a)
#endif
instance Functor Image where
fmap f img = img { imgPixels = amap f (imgPixels img) }
-- | Build a new image.
imgNew :: Int -- ^ Width
-> Int -- ^ Height
-> [((Int, Int), a)] -- ^ Coordinate-value list
-> Image a
imgNew width height entries = Image
{ imgWidth = width
, imgHeight = height
, imgPixels = array ((0, 0), (width - 1, height - 1)) entries
}
-- | Find out, if the given coordinates are within the 'Image'
-- borders (which are @ (0, 0) .. (width-1, height-1)@).
imgInRange :: Int -- ^ x-coordinate
-> Int -- ^ y-coordinate
-> Image a -- ^ An 'Image'
-> Bool -- ^ If @(x, y)@ is within the 'Image'
imgInRange x y img = 0 <= x && x < imgWidth img && 0 <= y && y < imgHeight img
-- | Access a pixel at given x/y-coordinates.
imgPixel :: Int -- ^ x-coordinate
-> Int -- ^ y-coordinate
-> Image a -> a
imgPixel x y img = (imgPixels img) ! (x, y)
-- | Set a pixel at given x/y-coordinates.
imgSetPixel :: Int -> Int -> a -> Image a -> Image a
imgSetPixel x y pixel img = img { imgPixels = (imgPixels img) // [((x, y), pixel)] }
-- | We'll just use 'Int's to identifiy labels.
type LabelKey = Int
-- | Stores compiler-relevant information about a label. This type
-- implements an instance of 'Monoid' to merge labels.
data LabelInfo
= EmptyInfo -- ^ The empty label
| LabelInfo
{ _labelSize :: {-# UNPACK #-} !Int -- ^ Number of pixels
, labelTop :: {-# UNPACK #-} !LabelBorder -- ^ Top border
, labelLeft :: {-# UNPACK #-} !LabelBorder -- ^ left border
, labelBottom :: {-# UNPACK #-} !LabelBorder -- ^ Bottom border
, labelRight :: {-# UNPACK #-} !LabelBorder -- ^ Right border
} -- ^ Label with a size and four borders
deriving (Show, Eq, Ord)
-- | Number of pixels in a label. This function is defined for all
-- constructors of 'LabelInfo' so, in contrast to '_labelSize', it
-- won't fail on 'EmptyInfo' .
labelSize :: LabelInfo -> Int
labelSize EmptyInfo = 0
labelSize _info@(LabelInfo { }) = _labelSize _info
instance Monoid LabelInfo where
mempty = EmptyInfo
mappend EmptyInfo i = i
mappend i EmptyInfo = i
mappend i1 i2 = LabelInfo
{ _labelSize = labelSize i1 + labelSize i2
, labelTop = mergeMin (labelTop i1) (labelTop i2)
, labelLeft = mergeMin (labelLeft i1) (labelLeft i2)
, labelBottom = mergeMax (labelBottom i1) (labelBottom i2)
, labelRight = mergeMax (labelRight i1) (labelRight i2)
}
-- | Holds information of a label (coloured area) relevant for the Piet
-- language, i. e. information about where the program flow will be
-- directed regarding a Direction Pointer.
--
-- Holds a border position (e. g. an x-coordinate) and the minimum
-- or maximum associated \"other\" coordinates (e. g. y-coordinates).
data LabelBorder = LabelBorder
{ borderCoord :: {-# UNPACK #-} !Int -- ^ Where the border is located
, borderMin :: {-# UNPACK #-} !Int -- ^ Minimum \"other\" coordinate of the border
, borderMax :: {-# UNPACK #-} !Int -- ^ Maximum \"other\" coordinate of the border
} deriving (Show, Eq, Ord)
-- | Merge two 'LabelBorder's holding a /maximum/ coordinate.
mergeMin :: LabelBorder -> LabelBorder -> LabelBorder
mergeMin = merge (comparing borderCoord)
-- | Merge two 'LabelBorder's holding a /minimum/ coordinate.
mergeMax :: LabelBorder -> LabelBorder -> LabelBorder
mergeMax = merge (comparing (negate . borderCoord))
-- | General merge, see 'mergeMin' and 'mergeMax'
merge :: (LabelBorder -> LabelBorder -> Ordering) -> LabelBorder -> LabelBorder -> LabelBorder
merge comp b1 b2 = case comp b1 b2 of
EQ -> b1
{ borderMin = min (borderMin b1) (borderMin b2)
, borderMax = max (borderMax b1) (borderMax b2)
}
LT -> b1
GT -> b2
-- | Add a pixel to a 'LabelInfo'.
addPixel :: Int -> Int -> LabelInfo -> LabelInfo
addPixel x y EmptyInfo = LabelInfo
{ _labelSize = 1
, labelTop = LabelBorder y x x
, labelLeft = LabelBorder x y y
, labelBottom = LabelBorder y x x
, labelRight = LabelBorder x y y
}
addPixel x y nonEmpty = nonEmpty
{ _labelSize = 1 + labelSize nonEmpty
, labelTop = mergeMin (labelTop nonEmpty) (LabelBorder y x x)
, labelLeft = mergeMin (labelLeft nonEmpty) (LabelBorder x y y)
, labelBottom = mergeMax (labelBottom nonEmpty) (LabelBorder y x x)
, labelRight = mergeMax (labelRight nonEmpty) (LabelBorder x y y)
}