packages feed

Mondrian-0.1.0.0: lib/Graphics/Rendering/Rect/Backgrounds.hs

module Graphics.Rendering.Rect.Backgrounds(Backgrounds(..), Pattern(..),
    RadialShape(..), Resize(..), Length(..), Extent(..),
    resolveSize, renderBackgrounds) where

import Graphics.Rendering.Rect.CSS.Backgrounds
import Graphics.Rendering.Rect.Types
import Graphics.Rendering.Rect.Image (Texture(texSize), textureSetRepeat)
import qualified Data.ByteString.Char8 as B8
import Linear (M44, V2(..))

import Control.Monad.IO.Class (MonadIO(..))
import Data.Maybe (fromMaybe, listToMaybe)
import Control.Monad (forM)

baseFragmentShader :: B8.ByteString
baseFragmentShader = B8.pack $ unlines [
    "#version 330 core",
    "out vec4 fcolour;",
    "uniform vec4 colour;",
    "void main() { fcolour = colour; }"
  ]

imageFragmentShader :: B8.ByteString
imageFragmentShader = B8.pack $ unlines [
    "#version 330 core",
    "in vec2 coord;",
    "out vec4 fcolour;",
    "uniform vec2 pos;",
    "uniform sampler2D image;",
    "uniform vec2 size;",
    "void main() { fcolour = texture(image, coord/size - pos/size); }"
  ]

linearFragmentShader :: B8.ByteString
linearFragmentShader = B8.pack $ unlines [
    "#version 330 core",
    "in vec2 coord;",
    "out vec4 fcolour;",
    "uniform vec2 size;",
    "uniform vec4 stops[10];",
    "uniform float stopPoints[10];",
    "uniform int nStops;",
    "uniform float angle;",
    "",
    "void main() {",
    "   vec2 pos = coord/size;", -- Range 0..1
    "   pos -= 0.5; pos *= 2;", -- Range -1..1
    "   float a = pos.x*sin(angle) + pos.y*-cos(angle);", -- Range -1..1
    "   a /= 2; a += 0.5;", -- Range 0..1
    "",
    "   int i = 0;",
    -- Workaround for buggy GPU drivers on test machine.
    "   if (1 < nStops - 1 && a > stopPoints[1]) i = 1;",
    "   if (2 < nStops - 1 && a > stopPoints[2]) i = 2;",
    "   if (3 < nStops - 1 && a > stopPoints[3]) i = 3;",
    "   if (4 < nStops - 1 && a > stopPoints[4]) i = 4;",
    "   if (5 < nStops - 1 && a > stopPoints[5]) i = 5;",
    "   if (6 < nStops - 1 && a > stopPoints[6]) i = 6;",
    "   if (7 < nStops - 1 && a > stopPoints[7]) i = 7;",
    "   if (8 < nStops - 1 && a > stopPoints[8]) i = 8;",
    "",
    "   a = smoothstep(stopPoints[i], stopPoints[i+1], a);",
    "   fcolour = mix(stops[i], stops[i+1], a);",
    "}"
  ]

radialFragmentShader :: B8.ByteString
radialFragmentShader = B8.pack $ unlines [
    "#version 330 core",
    "in vec2 coord;",
    "out vec4 fcolour;",
    "uniform vec2 size;",
    "uniform vec2 extent;",
    "uniform vec2 center;",
    "uniform vec4 stops[10];",
    "uniform float stopPoints[10];",
    "uniform int nStops;",
    "",
    "void main() {",
    "   float a = distance(coord/extent, center/size) * 2;",
    "",
    "   int i = 0;",
    -- Workaround for buggy GPU drivers on test machine.
    "   if (1 < nStops - 1 && a > stopPoints[1]) i = 1;",
    "   if (2 < nStops - 1 && a > stopPoints[2]) i = 2;",
    "   if (3 < nStops - 1 && a > stopPoints[3]) i = 3;",
    "   if (4 < nStops - 1 && a > stopPoints[4]) i = 4;",
    "   if (5 < nStops - 1 && a > stopPoints[5]) i = 5;",
    "   if (6 < nStops - 1 && a > stopPoints[6]) i = 6;",
    "   if (7 < nStops - 1 && a > stopPoints[7]) i = 7;",
    "   if (8 < nStops - 1 && a > stopPoints[8]) i = 8;",
    "",
    "   a = smoothstep(stopPoints[i], stopPoints[i+1], a);",
    "   fcolour = mix(stops[i], stops[i+1], a);",
    "}"
  ]
circleFragmentShader :: B8.ByteString
circleFragmentShader = B8.pack $ unlines [
    "#version 330 core",
    "in vec2 coord;",
    "out vec4 fcolour;",
    "uniform vec2 center;",
    "uniform float radius;",
    "uniform vec4 stops[10];",
    "uniform float stopPoints[10];",
    "uniform int nStops;",
    "",
    "void main() {",
    "   float a = distance(coord, center)/radius;",
    "",
    "   int i = 0;",
    -- Workaround for buggy GPU drivers on test machine.
    "   if (1 < nStops - 1 && a > stopPoints[1]) i = 1;",
    "   if (2 < nStops - 1 && a > stopPoints[2]) i = 2;",
    "   if (3 < nStops - 1 && a > stopPoints[3]) i = 3;",
    "   if (4 < nStops - 1 && a > stopPoints[4]) i = 4;",
    "   if (5 < nStops - 1 && a > stopPoints[5]) i = 5;",
    "   if (6 < nStops - 1 && a > stopPoints[6]) i = 6;",
    "   if (7 < nStops - 1 && a > stopPoints[7]) i = 7;",
    "   if (8 < nStops - 1 && a > stopPoints[8]) i = 8;",
    "",
    "   a = smoothstep(stopPoints[i], stopPoints[i+1], a);",
    "   fcolour = mix(stops[i], stops[i+1], a);",
    "}"
  ]

conicFragmentShader :: B8.ByteString
conicFragmentShader = B8.pack $ unlines [
    "#version 330 core",
    "in vec2 coord;",
    "out vec4 fcolour;",
    "uniform vec2 center;",
    "uniform float angle;",
    "uniform vec4 stops[10];",
    "uniform float stopPoints[10];",
    "uniform int nStops;",
    "",
    "void main() {",
    "   vec2 v = coord - center;",
    "   float a = atan(v.x, -v.y) - angle;",
    "   float turn = 2*radians(180);",
    "   a = fract(a/turn);",
    "",
    "   int i = 0;",
    -- Workaround for buggy GPU drivers on test machine.
    "   if (1 < nStops - 1 && a > stopPoints[1]) i = 1;",
    "   if (2 < nStops - 1 && a > stopPoints[2]) i = 2;",
    "   if (3 < nStops - 1 && a > stopPoints[3]) i = 3;",
    "   if (4 < nStops - 1 && a > stopPoints[4]) i = 4;",
    "   if (5 < nStops - 1 && a > stopPoints[5]) i = 5;",
    "   if (6 < nStops - 1 && a > stopPoints[6]) i = 6;",
    "   if (7 < nStops - 1 && a > stopPoints[7]) i = 7;",
    "   if (8 < nStops - 1 && a > stopPoints[8]) i = 8;",
    "",
    "   a = smoothstep(stopPoints[i], stopPoints[i+1], a);",
    "   fcolour = mix(stops[i], stops[i+1], a);",
    "}"
  ]

renderBackgrounds :: (MonadIO m, MonadIO n) =>
    n (Backgrounds Texture -> Rects -> M44 Float -> m ())
renderBackgrounds = do
    base <- renderRectWith baseFragmentShader ["colour"]
    layer <- renderRectWith imageFragmentShader ["size", "pos"]
    linear <- renderRectWith linearFragmentShader ["size", "angle",
            "stops", "stopPoints", "nStops"]
    ellipse <- renderRectWith radialFragmentShader ["size", "extent", "center",
            "stops", "stopPoints", "nStops"]
    circle <- renderRectWith circleFragmentShader ["center", "radius",
            "stops", "stopPoints", "nStops"]
    conic <- renderRectWith conicFragmentShader ["center", "angle",
            "stops", "stopPoints", "nStops"]
    return $ \self a b -> do
        base [] [c $ background self] (headDef borderBox $ clip self)
                (headDef paddingBox $ origin self) a b
        let layers = image self `zip` (clip self ++ repeat borderBox)
                `zip` (bgSize self ++ repeat (Size Auto Auto))
                `zip` (origin self ++ repeat paddingBox)
                `zip` (bgPos self ++ repeat (Absolute 0, Absolute 0))
                `zip` (bgRepeat self ++ repeat (True, True))
        _<-forM layers $ \(((((pat0, clip0), size0), origin0), pos0), repeat0) ->
          case pat0 of
            None -> return ()
            Img img0 -> do
                let sz = resolveSize (size $ clip0 a) (texSize img0) size0
                let pos' = (v2$l2f' pos0$size$clip0 a) - (v2$l2f' pos0 sz)
                textureSetRepeat img0 repeat0
                layer [img0] [u $ v2 sz, u pos'] clip0 origin0 a b
            Linear angle stops -> let size' = size $ clip0 a in linear [] [
                    u $ v2 $ size', u angle, cs 10 $ map fst stops,
                    us $ ls2fs size' $ map snd $ take 10 stops, u $ length stops
                ] clip0 origin0 a b
            -- FIXME: Incorporate resolveEllipseExtent without messing up center
            Radial Ellipse ext org stops -> let sz@(_,h) = size $ clip0 a in
                let (org', ext') = resolveEllipseExtent sz org ext in ellipse [] [
                    u $ v2 sz, u $ v2 ext', u $ v2 org', cs 10 $ map fst stops,
                    us $ ls2fs (0,h/2) $ map snd $ take 10 stops, u $ length stops
                ] clip0 origin0 a b
            Radial Circle ext org stops -> let sz@(w,h) = size $ clip0 a
                in let (org', r) = resolveCircleExtent sz org ext in circle [] [
                    u $ v2 org', u r, cs 10 $ map fst stops,
                    us $ ls2fs (0,min w h/2) $ map snd $ take 10 stops,
                    u $ length stops
                ] clip0 origin0 a b
            Conical angle org stops -> let sz = size $ clip0 a in conic [] [
                    u $ v2 $ l2f' org sz, u angle, cs 10 $ map fst stops,
                    us $ ls2fs (0,2*pi) $ map snd $ take 10 stops, u $ length stops
                ] clip0 origin0 a b
        return ()

headDef :: c -> [c] -> c
headDef def = fromMaybe def . listToMaybe
v2 :: (a, a) -> V2 a
v2 = uncurry V2
-- Easier to express this algorithm on CPU-side...
ls2fs :: (Float, Float) -> [Length] -> [Float]
ls2fs (_,h) ls = resolveAutos 0 $ inner True 0 ls
  where
    -- https://drafts.csswg.org/css-images/#color-stop-fixup Step 1.
    inner True _ (Auto:ls') = Scale 0:inner False 0 ls'
    inner _ _ [Auto] = [Scale 1]
    -- Step 2
    inner _ prev (Scale x:ls') | x < prev = Scale prev:inner False prev ls'
    inner _ prev (Absolute x:ls') | x/h < prev = Scale prev:inner False prev ls'
    inner _ _ (Scale x:ls') = Scale x:inner False x ls'
    inner _ _ (Absolute x:ls') = Absolute x:inner False (x/h) ls'
    inner _ prev (Auto:ls') = Auto:inner False prev ls'
    inner _ _ [] = []
    -- Step 3
    resolveAutos :: Float -> [Length] -> [Float]
    resolveAutos _ (Scale x:ls') = x:resolveAutos x ls'
    resolveAutos _ (Absolute x:ls') = (x/h):resolveAutos (x/h) ls'
    resolveAutos _ [] = []
    resolveAutos prev ls0 = [prev + succ i*grad | i <- [0..n - 1]] ++ fs
      where
        (autos, ls') = span (==Auto) ls0
        n = toEnum $ length autos
        fs = resolveAutos 0 ls' -- Doesn't matter if prev's in another branch...
        next | (x:_) <- fs = x
            | otherwise = 1 -- Step 1 should've taken care of this...
        grad = (next - prev)/(n + 1)

l2f :: Length -> Float -> Float
l2f Auto x = x/2
l2f (Scale x) y = x*y
l2f (Absolute x) _ = x
l2f' :: (Length, Length) -> (Float, Float) -> (Float, Float)
l2f' (x,y) (w,h) = (l2f x w, l2f y h)

resolveEllipseExtent :: (Float, Float) -> (Length, Length) -> Extent ->
    ((Float, Float), (Float, Float))
resolveEllipseExtent sz@(x',y') pos ext = ((x, y), inner ext)
  where
    (x,y) = l2f' pos sz
    horiz = [x, x' - x]
    vert = [y, y' - y]
    inner (Extent s t) = (l2f s x, l2f t y)
    -- FIXME: How to calculate closest/farthest-corner?
    -- Spec just says keep this aspect ratio.
    inner ClosestCorner = (minimum horiz * 2, minimum vert * 2)
    inner ClosestSide = (minimum horiz * 2, minimum vert * 2)
    inner FarthestCorner = (maximum horiz * 2, maximum vert * 2)
    inner FarthestSide = (maximum horiz * 2, maximum vert * 2)
resolveCircleExtent :: (Float, Float) -> (Length, Length) -> Extent ->
    ((Float, Float), Float)
resolveCircleExtent sz@(x',y') pos ext = ((x, y), inner ext)
  where
    (x,y) = l2f' pos sz
    sides = [x, x' - x, y, y' - y]
    corners = [hypot x y, hypot x $ y'-y, hypot y $ x'-x, hypot (x'-x) (y'-y)]
    hypot a b = sqrt $ a*a + b*b
    inner (Extent a _) = l2f a y -- Should be absolute...
    inner ClosestCorner = minimum corners
    inner ClosestSide = minimum sides
    inner FarthestCorner = maximum corners
    inner FarthestSide = maximum sides