hasmin-1.0.2: src/Hasmin/Types/Gradient.hs
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : Hasmin.Types.Gradient
-- Copyright : (c) 2017 Cristian Adrián Ontivero
-- License : BSD3
-- Stability : experimental
-- Portability : non-portable
--
-----------------------------------------------------------------------------
module Hasmin.Types.Gradient
( Gradient(..)
, Side(..)
, ColorStop(..)
, Size(..)
, Shape(..)
) where
import Control.Monad.Reader (Reader, ask)
import Data.Monoid ((<>))
import Data.Text.Lazy.Builder (singleton)
import Data.Maybe (catMaybes, fromJust, isNothing, isJust)
import Data.Either (isLeft)
import Hasmin.Class
import Hasmin.Config
import Hasmin.Types.Color
import Hasmin.Types.Dimension
import Hasmin.Types.Numeric
import Hasmin.Types.PercentageLength
import Hasmin.Types.Position
import Hasmin.Utils
-- | CSS <side-or-corner> data type
data Side = LeftSide | RightSide | TopSide | BottomSide
deriving (Show, Eq)
instance ToText Side where
toBuilder LeftSide = "left"
toBuilder RightSide = "right"
toBuilder TopSide = "top"
toBuilder BottomSide = "bottom"
-- Possible pair of values, as expected by linear-gradient()
type SideOrCorner = (Side, Maybe Side)
-- | CSS <color-stop> data type
data ColorStop = ColorStop { csColor :: Color
, colorHint :: Maybe PercentageLength
} deriving (Show, Eq)
instance ToText ColorStop where
toBuilder (ColorStop c mpl) = toBuilder c <> maybe mempty f mpl
where f (Left p) = singleton ' ' <> toBuilder p
f (Right l) = singleton ' ' <> toBuilder l
instance Minifiable ColorStop where
minify (ColorStop c mlp) = do
newC <- minify c
newMlp <- (mapM . mapM) minify mlp
pure $ ColorStop newC newMlp
-- minifies color hints in a \<color-stops\> list
minifyColorHints :: [ColorStop] -> [ColorStop]
minifyColorHints [c1,c2] = [newC1, newC2]
where ch1 = colorHint c1
ch2 = colorHint c2
newC1
| isJust ch1 && isZero (fromJust ch1) = c1 {colorHint = Nothing}
| otherwise = c1
newC2
| ch2 == Just (Left (Percentage 100)) = c2 {colorHint = Nothing}
| ch2 `notGreaterThan` ch1 = c2 {colorHint = Just $ Right NullLength}
| otherwise = c2
minifyColorHints (c@(ColorStop a x):xs) = case x of
Nothing -> c : analyzeList (Left $ Percentage 0) 1 (c:xs) xs
Just y -> if isZero y
then ColorStop a Nothing : analyzeList y 1 (c:xs) xs
else c: analyzeList y 1 (c:xs) xs
minifyColorHints xs = error ("invalid <color-stop> list: " ++ show xs)
-- Returns True if the first value is equal or less than the second one, and False
-- otherwise, or if a comparison isn't possible.
notGreaterThan :: Maybe PercentageLength -> Maybe PercentageLength -> Bool
y `notGreaterThan` x
| isNothing x || isZero (fromJust x) = notPositive y
| otherwise = case fromJust x of
Left p -> maybe False (either (<= p) (const False)) y
Right d -> maybe False (either (const False) (notGreaterThanLength d)) y
where notPositive = maybe False (either (<= 0) notPositiveLength)
notPositiveLength (Length d _) = d <= 0
notPositiveLength NullLength = True
notGreaterThanLength NullLength NullLength = True
notGreaterThanLength NullLength (Length r _) = r <= 0
notGreaterThanLength (Length r _) NullLength = 0 <= r
notGreaterThanLength (Length r1 u1) (Length r2 u2)
| u1 == u2 = r2 <= r1
| isRelative u1 || isRelative u2 = False
| otherwise = toInches r2 u2 <= toInches r1 u1
-- Gathers at least three color stops to interpolate between the first and
-- last, and see if the middle one can be removed. As long as the color hints
-- are Nothing, keeps accumulating until it finds a Just it can use to interpolate.
analyzeList :: PercentageLength -> Int -> [ColorStop]
-> [ColorStop] -> [ColorStop]
analyzeList start n list (ColorStop _ mpl:xs)
| n < 2 = analyzeList start (n+1) list xs
| otherwise =
case mpl of
Just y -> let (newList, remainingList, startVal) = minifySegment start y n list
in newList ++ analyzeList startVal 2 remainingList xs
Nothing -> analyzeList start (n+1) list xs
analyzeList start n list [] =
case mpl of
Just (Left (Percentage 100)) -> [ColorStop x Nothing]
Nothing -> let end = Left $ Percentage 100
(newList, _, _) = minifySegment start end (n-1) list
in newList ++ [(last list) {colorHint = Nothing}]
_ -> [c]
where c@(ColorStop x mpl) = last list
-- Given two values and a count of values, uses them to create a list of
-- interpolated values, and based on the list decides if it should remove a
-- color hint or not.
minifySegment :: PercentageLength -> PercentageLength -> Int -> [ColorStop]
-> ([ColorStop], [ColorStop], PercentageLength)
minifySegment start end n list
| all isPercentage segment = handlePercentages (fromLeft' start) (fromLeft' end) n remainingList
-- add here support for dimension interpolation
| otherwise = (take (n-1) remainingList, remainingList, fromJust $ colorHint (head remainingList))
where segment = take (n+1) list
(_, remainingList) = splitAt (n-1) list
isPercentage x = maybe True isLeft (colorHint x)
-- Handles the minification of color hint values between percentages
handlePercentages :: Percentage -> Percentage -> Int
-> [ColorStop] -> ([ColorStop], [ColorStop], PercentageLength)
handlePercentages start end n remainingList =
let newList = zipWith simplifyValue remainingList interpolation
in (newList, remainingList, Left newStartVal)
where newStartVal = maybe (last interpolation) fromLeft' (colorHint $ head remainingList)
step = (end - start) / toPercentage n
interpolation = [start + toPercentage x * step | x <- [1..n-1]]
simplifyValue (ColorStop x mpl) y = ColorStop x $ mpl >>= \v ->
if fromLeft' v == y
then Nothing
else if fromLeft' v <= start
then Just $ Right NullLength
else Just v
-- | CSS <https://drafts.csswg.org/css-images-3/#typedef-gradient \<gradient\>>
-- data type.
data Gradient = OldLinearGradient (Maybe (Either Angle SideOrCorner)) [ColorStop]
-- OldLinearGradient is for the old syntax. It should eventually be deleted.
| LinearGradient (Maybe (Either Angle SideOrCorner)) [ColorStop]
| RadialGradient (Maybe Shape) (Maybe Size) (Maybe Position) [ColorStop]
-- TODO: replace with Maybe (These Shape Size)
deriving (Show)
-- ,| RepeatingLinearGradient
-- ,| RepeatingRadialGradient
{-
radial-gradient() = radial-gradient(
[ <ending-shape> || <size> ]? [ at <position> ]? ,
<color-stop-list>
)
radial-gradient(
[ [ circle || <length> ] [ at <position> ]? , |
[ ellipse || [ <length> | <percentage> ]{2} ] [ at <position> ]? , |
[ [ circle | ellipse ] || <extent-keyword> ] [ at <position> ]? , |
at <position> ,
]?
<color-stop> [ , <color-stop> ]+
)
where <extent-keyword> = closest-corner | closest-side | farthest-corner | farthest-side
and <color-stop> = <color> [ <percentage> | <length> ]?
-}
-- | CSS <https://drafts.csswg.org/css-images-3/#typedef-size \<size\>> data
-- type, used by @radial-gradient()@.
data Size = ClosestCorner
| ClosestSide
| FarthestCorner
| FarthestSide
| SL Length
| PL PercentageLength PercentageLength
deriving (Eq, Show)
instance ToText Size where
toBuilder ClosestCorner = "closest-corner"
toBuilder ClosestSide = "closest-side"
toBuilder FarthestCorner = "farthest-corner"
toBuilder FarthestSide = "farthest-side"
toBuilder (SL d) = toBuilder d
toBuilder (PL pl1 pl2) = toBuilder pl1 <> singleton ' ' <> toBuilder pl2
-- TODO rename to EndingShape
-- | CSS <https://drafts.csswg.org/css-images-3/#valdef-radial-gradient-ending-shape \<ending-shape\>> data type, used by @radial-gradient()@.
data Shape = Circle | Ellipse
deriving (Eq, Show)
instance ToText Shape where
toBuilder Circle = "circle"
toBuilder Ellipse = "ellipse"
-- If the argument is to top, to right, to bottom, or to left, the angle of
-- the gradient line is 0deg, 90deg, 180deg, or 270deg, respectively.
instance Minifiable Gradient where
minify g@(OldLinearGradient x cs) = do
conf <- ask
case gradientSettings conf of
GradientMinOn -> do css <- mapM minify cs
pure $ OldLinearGradient x (minifyColorHints css)
GradientMinOff -> pure g
minify g@(LinearGradient x cs) = do
conf <- ask
case gradientSettings conf of
GradientMinOn -> do css <- mapM minify cs
newX <- minifyAngleOrSide x
pure $ LinearGradient newX (minifyColorHints css)
GradientMinOff -> pure g
minify g@(RadialGradient sh sz p cs) = do
conf <- ask
case gradientSettings conf of
GradientMinOn -> do css <- mapM minify cs
let np = minifyRadialPosition True {-shouldMinifyPosition conf-} p
pure $ minShapeAndSize sh sz np (minifyColorHints css)
GradientMinOff -> pure g
-- If a single length was used, the default shape is circle, otherwise ellipse.
-- circle farthest-corner == circle
-- ellipse farthest-corner == ellipse == farthest-corner
minShapeAndSize :: Maybe Shape -> Maybe Size -> Maybe Position -> [ColorStop] -> Gradient
minShapeAndSize (Just Circle) sz@(Just (SL _)) = RadialGradient Nothing sz
minShapeAndSize (Just Circle) (Just FarthestCorner) = RadialGradient (Just Circle) Nothing
minShapeAndSize (Just Ellipse) sz@(Just (PL _ _)) = RadialGradient Nothing sz
minShapeAndSize (Just Ellipse) (Just FarthestCorner) = RadialGradient Nothing Nothing
minShapeAndSize (Just Ellipse) sz@(Just _) = RadialGradient Nothing sz
minShapeAndSize (Just Ellipse) Nothing = RadialGradient Nothing Nothing
minShapeAndSize Nothing (Just FarthestCorner) = RadialGradient Nothing Nothing
minShapeAndSize x sz = RadialGradient x sz
-- Minifies the position in the radial gradient based on the position
-- minification settings. If positions should be minified, and if it is
-- equivalent to 'center', it is removed. If positions should not be minified,
-- it still removes it if it is equivalent to 'center', but leaves it untouched
-- otherwise.
minifyRadialPosition :: Bool -> Maybe Position -> Maybe Position
minifyRadialPosition _ Nothing = Nothing
minifyRadialPosition cond (Just p)
| minifiedPos == centerPos = Nothing
| cond = Just minifiedPos
| otherwise = Just p
where centerPos = Position Nothing p50 Nothing Nothing
minifiedPos = minifyPosition p
minifyAngleOrSide :: Maybe (Either Angle SideOrCorner)
-> Reader Config (Maybe (Either Angle SideOrCorner))
minifyAngleOrSide Nothing = pure Nothing
minifyAngleOrSide (Just (Left a))
| a == defaultGradientAngle = pure Nothing
| otherwise = (Just . Left) <$> minify a
where defaultGradientAngle = Angle 180 Deg
minifyAngleOrSide (Just (Right b))
| b == defaultGradientSideOrCorner = pure Nothing
| otherwise = pure $ Just (minifySide b)
where defaultGradientSideOrCorner = (BottomSide, Nothing)
minifySide (TopSide, Nothing) = Left NullAngle
minifySide (RightSide, Nothing) = Left (Angle 90 Deg)
minifySide (BottomSide, Nothing) = Left (Angle 180 Deg)
minifySide (LeftSide, Nothing) = Left (Angle 270 Deg)
minifySide z = Right z
instance ToText Gradient where
toBuilder (OldLinearGradient mas csl) = maybe mempty f mas
<> mconcatIntersperse id (singleton ',') (fmap toBuilder csl)
where f = either ((<> singleton ',') . toBuilder) g
g (s, ms) = toBuilder s
<> maybe mempty (\x -> singleton ' ' <> toBuilder x) ms
<> singleton ','
toBuilder (LinearGradient mas csl) = maybe mempty f mas
<> mconcatIntersperse id (singleton ',') (fmap toBuilder csl)
where f = either ((<> singleton ',') . toBuilder) g
g (s, ms) = "to " <> toBuilder s
<> maybe mempty (\x -> singleton ' ' <> toBuilder x) ms
<> singleton ','
toBuilder (RadialGradient sh sz p cs) = firstPart
<> mconcatIntersperse id (singleton ',') (fmap toBuilder cs)
where l = catMaybes [fmap toBuilder sh, fmap toBuilder sz, fmap (\x -> "at " <> toBuilder x) p]
firstPart
| null l = mempty
| otherwise = mconcatIntersperse id (singleton ' ') l <> singleton ','
instance Eq Gradient where
LinearGradient x1 csl1 == LinearGradient x2 csl2 =
handleMaybe x1 x2 && csl1 == csl2
where handleMaybe Nothing Nothing = True
handleMaybe (Just x) (Just y) = handleEither x y
handleMaybe _ _ = False
handleEither (Left a1) (Left a2) = a1 == a2
handleEither (Left a) (Right s) = angleSideEq a s
handleEither (Right s) (Left a) = angleSideEq a s
handleEither s1 s2 = s1 == s2
LinearGradient{} == _ = False
_ == LinearGradient{} = False
_ == _ = False -- TODO implement other comparisons
angleSideEq :: Angle -> SideOrCorner -> Bool
angleSideEq (Angle 90 Deg) (RightSide, Nothing) = True
angleSideEq (Angle 180 Deg) (BottomSide, Nothing) = True
angleSideEq (Angle 270 Deg) (LeftSide, Nothing) = True
angleSideEq a (TopSide, Nothing)
| isZeroAngle a = True
| otherwise = False
angleSideEq _ _ = False