Difference between revisions of "TW-Logo-Haskell"

From HaskellWiki
Jump to navigation Jump to search
(Original post of Haskell code)
 
m
 
Line 6: Line 6:
 
{-# LANGUAGE FlexibleInstances #-}
 
{-# LANGUAGE FlexibleInstances #-}
   
import List (intersperse)
 
 
import Control.Monad (forM)
 
import Control.Monad (forM)
   
Line 316: Line 315:
 
g = byteToHex $ round (255 * green col)
 
g = byteToHex $ round (255 * green col)
 
b = byteToHex $ round (255 * blue col)
 
b = byteToHex $ round (255 * blue col)
 
unspaces pieces = concat $ intersperse " " pieces
 
   
 
-- see http://www.w3.org/TR/SVG11/paths.html#PathDataGeneralInformation
 
-- see http://www.w3.org/TR/SVG11/paths.html#PathDataGeneralInformation
Line 326: Line 323:
 
showPt (x,y) = show x++" "++show y
 
showPt (x,y) = show x++" "++show y
   
pathToStr path = unspaces [cmdToStr c | c <- path]
+
pathToStr path = unwords [cmdToStr c | c <- path]
   
 
pathToSVG outline strokeWid (col,path) =
 
pathToSVG outline strokeWid (col,path) =

Latest revision as of 11:26, 21 February 2010

Thompson-Wheeler logo in Haskell

The following Haskell script (originally by Jared Updike) will create a number of SVG files with variations in color and style.

{-# LANGUAGE FlexibleInstances #-}

import Control.Monad (forM)

----------------------------------------------
-- VARIATIONS

-- YOU: feel free to play with the variations
sizes      = [ (16,16), (32,32), (64,64), (128,128), (256, 256) ]
variations = [ ( "normal-partround-lambda-col1", sizes, col1 opt0 ),
               ( "normal-partround-lambda-col2", sizes, col2 opt0 ),
               ( "normal-partround-lambda",      sizes, opt0 ),
               ( "bold-round-lambda",            sizes, opt3 ),
               ( "bold-lambda-on-top",           sizes, opt5 ),
               ( "bold-bind-on-top",             sizes, opt1 ),
               ( "skinny-lambda-on-top",         sizes, opt2 ),
               ( "skinny-round-lambda",          sizes, opt4 ),
               ( "outline-bold",                 sizes, outline opt5 ),
               ( "outline-normal-partrounded",   sizes, partRounded $ outline opt0 ),
               ( "outline-skinny-partrounded",   sizes, partRounded $ outline opt2 ) ]

main = do
   let flatVars = flattened variations
   forM flatVars $ \ (fname, size, opt, _) -> do
      let svgStr = svgShapes size opt
      writeFile fname svgStr
   let htmlChunk triple@(desc,_,_) =
        let embed (fname, (sizex,sizey), _, _) =
               "<td><embed src=\""++fname++"\" width='"++show (round sizex)++
                  "' height='"++show (round sizey)++"' /></td>\n"
            insides = concatMap embed (flattened [triple])
          in "<tr>\n<td>"++desc++"</td>\n"++insides++"</tr>\n"
   let svgHTML = "<html><title>Haskell Logo variations</title><body>\n"++
                   "<table>\n" ++ concatMap htmlChunk variations ++ "\n</table>\n</body>\n</html>"
   let htmlOut = "svg-test.html"
   writeFile htmlOut svgHTML
   putStrLn$ "Wrote out files. Point your browser at "++htmlOut

flattened vars =
  [ (desc ++ "-" ++ show (round sizex) ++ ".svg", size, opt, desc) |
     (desc, size@(sizex, _), opt) <- flatten vars ]
flatten vars = do
   (desc, sizes, opt) <- vars
   size <- sizes
   return (desc, size, opt)

opt0 = partRounded optNormal
opt1 = optBold { lambda = leg }
opt2 = optSkinny
opt3 = rounded optBold
opt4 = rounded opt2
opt5 = optBold

data Options a = Opt
     { hh, bb, kk, kk2, tt, tt2, ee, kk3 :: a,
       roundness, looseness :: a,
       monochrome, lambda, overlap, nofill, roundCorners, allRound :: Bool,
       color1, color2 :: RGB a }
optSkinny = optBold { kk = 0.8, kk2 = 0.8*sqrt2*0.7, kk3 = 0.8/sqrt2,
                      tt = 1.5/2, tt2 = 1.5/2/sqrt2, roundness = 0.2 }
optNormal = optBold { kk = 0.8, kk2 = 0.8*sqrt2*0.7, kk3 = 0.8/sqrt2,
                      tt = 2.5/2, tt2 = 2.5/2/sqrt2, roundness = 0.2 }

outline opt = opt { nofill = True, monochrome = True, lambda = True }
rounded opt = opt { roundCorners = True }
partRounded opt = opt { roundCorners = True, allRound = False }

black = RGB 0.0 0.0 0.0
gray = RGB 0.1 0.1 0.1
gray1 = RGB (75/255) (84/255) (87/255)
gray2 = RGB (113/255) (127/255) (129/255)
khaki1 = RGB (175/255) (167/255) (159/255)
khaki2 = RGB (215/255) (210/255) (203/255)
teal = RGB (60/255) (130/255) (132/255)
royal = RGB (73/255) (21/255) (123/255)
dteal = RGB (70/255) (120/255) (122/255)
droyal = RGB (63/255) (41/255) (113/255)
col1 opt = opt { color1 = droyal, color2 = dteal }
col2 opt = opt { color1 = khaki1, color2 = khaki2 }

leg = False
optBold = Opt {
   -- COLOR/STYLE ISSUES
   -- could force colors to be the same as color1
   monochrome = False,
   -- could do only outlines, no fill (best with monochrome==True)
   nofill = False,
   -- draw the leg under the right '>' sign (==True, recommended) or just touch shapes
   overlap = True,
   -- rounded corners?
   roundness    = 0.3,  -- 0 = start curving at midpoint, 1 = no curve at all
   looseness    = 0.7,  -- 0 = curve control points sit atop one another, 1 = floppy, loose curves (higher radius)
   roundCorners = False,
   allRound     = True,
   -- style: lambda on top (colorwise, lambda solid color) or '>>' on top (leg its own color)
   lambda = True,
   -- basic colors
   color1 = gray1,
   color2 = gray2,

   -- DESIGN/SHAPE
   hh = 6.0,                                   -- height
   bb = 2.0,                                   -- horizontal distance between left end of '>' and right point of '>'
   kk = 0.5, kk2 = 0.5*sqrt2, kk3 = 0.5/sqrt2, -- padding values
   tt = 1.5, tt2 = 1.5/sqrt2,                  -- thickness of various parts (horizontal and diagonal, respectively)
   ee = 2.5                                    -- length of equals sign
   }

----------------------------------------------
-- Mini EDSL for creating SVGs

instance Num a => Num (a,a) where
   (x0,y0) + (x1,y1) = (x0+x1, y0+y1)
   (x0,y0) - (x1,y1) = (x0-x1, y0-y1)
   (x0,y0) * (s,_) = (s*x0, s*y0) -- note that this is a hack!
   fromInteger i = (fromInteger i,0) -- this hack goes with the above hack
   negate (x,y) = (negate x, negate y)
   abs (x,y) = error "abs of tuple undefined"
   signum (x,y) = error "signum of tuple undefined"

instance (Num a, Fractional a) => Fractional (a,a) where
   recip (x0,y0) = error "recip of tuple undefiened"
   (x0,y0) / (x1,_) = (x0/x1, y0/x1)
   fromRational r = (fromRational r,0)

type Pt a = (a,a)
data Command a =
   Move (Pt a)
   | LineTo (Pt a)
   | Control { c1, c2, nextPt :: (Pt a) }
   | Cycle
     deriving (Show, Eq)

-- type Path a = [Command a] -- too general! 'a' must be Show-able!!!!!!
type Path = [Either (Command Double) (Command Double)]

m p = Move p
z = Cycle
l p = LineTo p
c p1 p2 pNext = Control p1 p2 pNext

data RGB a = RGB { red, green, blue :: a } deriving (Eq, Show)

----------------------------------------------
-- CUSTOM HASKELL LOGO

sqrt2 = 1.4142

unEither (Left  x) = x
unEither (Right x) = x

-- like LineTo (or 'l') but here we draw the line and a control curve around the end point
roundLineTo (roundness,looseness) prevW meW nextW =
   let me       = unEither meW
       prev     = unEither prevW
       next     = unEither nextW
       sharp    = case meW of { Left _ -> True; Right _ -> False }
       percMe   = 1/2 + (1-roundness)/2
       nearMe   = me `times` percMe + prev `times` (1-percMe)
       nearMe'  = me `times` percMe + next `times` (1-percMe)
       ctrlPerc = 1/2 + (1-looseness)/2
       me1      = nearMe `times` (1-ctrlPerc) + me `times` ctrlPerc
       me2      = nearMe' `times` (1-ctrlPerc) + me `times` ctrlPerc
    in if sharp
         then [l me, l nearMe']
         else [l nearMe, c me1 me2 nearMe']

makeRound' parms first (sndlast:lst:[]) =
   roundLineTo parms sndlast lst first
makeRound' parms first (prev:(rest@(me:next:_))) =
   roundLineTo parms prev me next ++ makeRound' parms first rest
makeRound' parms first _ = error "Expected at least three points for makeRound'"

pt `times` s = (s * fst pt, s * snd pt)

makeRoundBase parms@(roundness,looseness) (rest@(firstW:secondW:_)) =
   let percMe        = 1/2 + (1-roundness)/2
       lstW          = last rest
       lst           = unEither lstW
       first         = unEither firstW
       -- second        = unEither secondW
       startNearLast = lst `times` percMe + first `times` (1-percMe)
       firstLineTo   = roundLineTo parms lstW firstW secondW
     in [m startNearLast] ++ firstLineTo ++ makeRound' parms firstW rest --    ++ [z]  -- not needed since we manually reconnect

-- pass on the sharpPt (Left) / roundPt (Right) information
getPoints []                      = []
getPoints (Left Cycle:[])         = []
getPoints (Right Cycle:[])        = []
getPoints ((Left  (LineTo pt)):l) = Left pt  : getPoints l
getPoints ((Right (LineTo pt)):l) = Right pt : getPoints l
getPoints ((Left  (Move pt)):l)   = Left pt  : getPoints l
getPoints ((Right (Move pt)):l)   = Right pt : getPoints l
getPoints _ = error "Badly formed input: makeRound expects a shape ending with a Z (Cycle), containing no Control points, just Move and LineTo"

-- make all points rounded! (Right)
getPointsR []                      = []
getPointsR (Left Cycle:[])         = []
getPointsR (Right Cycle:[])        = []
getPointsR ((Left  (LineTo pt)):l) = Right pt  : getPointsR l
getPointsR ((Right (LineTo pt)):l) = Right pt : getPointsR l
getPointsR ((Left  (Move pt)):l)   = Right pt  : getPointsR l
getPointsR ((Right (Move pt)):l)   = Right pt : getPointsR l
getPointsR _ = error "Badly formed input: makeRound expects a shape ending with a Z (Cycle), containing no Control points, just Move and LineTo"

makeRound getpts parms list = makeRoundBase parms (getpts list)

-- the heart of the Logo
shapes opt = let
   -- colors
   col1 = color1 opt
   col2 = if monochrome opt then col1 else color2 opt
   round = roundCorners opt
   getPoints' = if allRound opt then getPointsR else getPoints
   rounder = if round then makeRound getPoints' parms else map unEither
   -- NOTE: wrapping Either for prounding (Partial Rounding): Left=point is 'left' alone, Right=round that point
   roundPt = Right
   sharpPt = Left -- point 'left' sharp
   parms = (roundness opt, looseness opt)

   -- constants
   h = hh opt; b = bb opt; e = ee opt
   k = kk opt; k2 = kk2 opt; k3 = kk3 opt
   t = tt opt; t2 = tt2 opt
   s = k3 + t2

   rstroke = (t/2,0)
   lstroke = (-t/2,0)
   
   -- skeleton 0 : left '>' symbol
   skel0 = [(0,0), (b,h/2), (0,h)]

   leftGT = leftGT' skel0
   leftGT' s0 = rounder [roundPt$ m$ s0!!0 + rstroke, sharpPt$ l$ s0!!1 + rstroke, roundPt$ l$ s0!!2 + rstroke,
                         roundPt$ l$ s0!!2 + lstroke, sharpPt$ l$ s0!!1 + lstroke, roundPt$ l$ s0!!0 + lstroke, roundPt z]

   -- skeleton 1 : right '>' symbol
   skel1 = [(b+t+k-b,0), (b+t+k,h/2), (b+t+k-b,h)]
   rightGT = leftGT' skel1

   -- skeleton 2 : lambda and/or leg
   skel2 = [ skel1!!1, (fst(skel1!!0) + 2*b, snd(skel1!!0)) ]
   why x = let
      bl = skel2!!1 + lstroke
      tl = skel1!!2 + lstroke
      dx = fst tl - fst bl
      dy = h
      y  = dy * (x - fst bl) / dx + snd bl
      in   y
   crosspt = let x = fst(skel2!!0) in (x, why x)

   lam = rounder [roundPt$ m$ skel1!!0 + rstroke, sharpPt$ l$ crosspt,
          roundPt$ l$ skel2!!1 + lstroke, roundPt$ l$ skel2!!1 + rstroke,
          roundPt$ l$ skel1!!2 + rstroke, roundPt$ l$ skel1!!2 + lstroke,
          sharpPt$ l$ skel1!!1 + lstroke, roundPt$ l$ skel1!!0 + lstroke, roundPt z]
   leg' = [roundPt$ m$ skel2!!0 + rstroke, sharpPt$ l$ skel2!!1 + rstroke,
           sharpPt$ l$ skel2!!1 + lstroke]
   leg = if overlap opt
           then rounder $ leg' ++ [roundPt$ l$ skel2!!0 + lstroke, roundPt z]
           else rounder $ leg' ++ [sharpPt$ l$ crosspt, roundPt z]

   -- calculate the slanted part of the equals sign
   eqCtrLft = skel1!!1 + (t2/2+k2,0)
   eqCtrRgt = eqCtrLft + (e,0)

   farRight = fst eqCtrRgt

   eks y = fst eqCtrLft + 2*b*(h/2 - y)/h
   xy y = (eks y, y)

   -- skel3 = [ xy (h/2+s/2), eqCtrRgt + (0,s/2) ] -- skeleton or guide to bisect equals sign
   -- the two pieces of the equals sign
   eqTop = rounder [ sharpPt$ m$ xy (h/2+s/2-t2/2), roundPt$ l$ (farRight,h/2+s/2-t2/2),
            roundPt$ l$ (farRight,h/2+s/2+t2/2), sharpPt$ l$ xy (h/2+s/2+t2/2), roundPt z]
   eqBot = rounder [ sharpPt$ m$ xy (h/2-s/2-t2/2), roundPt$ l$ (farRight,h/2-s/2-t2/2),
             roundPt$ l$ (farRight,h/2-s/2+t2/2), sharpPt$ l$ xy (h/2-s/2+t2/2), roundPt z]

   boundsBL = (-1.5*t,-t)
   boundsBR = (farRight+t,-t)
   boundsTR = (farRight+t,h+t)
   boundsTL = (-1.5*t,h+t)

   bounds = (boundsBL,boundsTR)

   in if lambda opt
        then (bounds, [(col1, leftGT), (col2, lam), (col1, eqTop), (col1, eqBot)])
        else (bounds, [(col1, leftGT), (col2, leg), (col1, rightGT), (col1, eqTop), (col1, eqBot)])

----------------------------------------------
-- SVG string concatenation code

svgXMLheaders = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" ++
     "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">" ++ "\n"

hexChars = "0123456789abcdef"
byteToHex b | b <= 0    = "00"
            | b >= 255  = "ff"
            | otherwise = big:small:[]
                  where big = hexChars !! hi
                        small = hexChars !! lo
                        hi = b `div` 16
                        lo = b `mod` 16

svgColorToStyle outline col thick =
   if outline
     then "fill=\"none\" stroke=\""++c++"\" stroke-width=\""++show thick++"\""
     else "fill=\""++c++"\" stroke=\""++c++"\" stroke-width=\""++show thick++"\""
     where c = "#" ++ r ++ g ++ b
           r = byteToHex $ round (255 * red col)
           g = byteToHex $ round (255 * green col)
           b = byteToHex $ round (255 * blue col)

-- see http://www.w3.org/TR/SVG11/paths.html#PathDataGeneralInformation
cmdToStr (Move p)          = "M "++showPt p
cmdToStr (LineTo p)        = "L "++showPt p
cmdToStr (Control c1 c2 p) = "C "++showPt c1++" "++showPt c2++" "++showPt p
cmdToStr Cycle             = "z"
showPt (x,y) = show x++" "++show y

pathToStr path = unwords [cmdToStr c | c <- path]

pathToSVG outline strokeWid (col,path) =
   "<path d=\""++d++"\" "++style++" />"
   where style = svgColorToStyle outline col strokeWid
         d     = pathToStr path

flipCmd f (Move p) = Move (f p)
flipCmd f (LineTo p) = LineTo (f p)
flipCmd f (Control p q r) = Control (f p) (f q) (f r)
flipCmd f Cycle = Cycle
flipPath f path = [flipCmd f c | c <- path]
flipPaths f paths = [(col,flipPath f p) | (col,p) <- paths]

svg (w,h) box title desc paths =
--   "<svg width=\""++w++"\" height=\""++h++"\" viewBox=\""++box++"\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\">\
 "<svg width=\""++w++"\" height=\""++h++"\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\">\
   \\n  <title>"++title++"</title>\
   \\n  <desc>"++desc++"</desc>"++ "\n"
   ++ paths ++ "\n" ++
   "</svg>" ++"\n"

pathsToSVG strokeWid wh box outline paths =
   svgXMLheaders ++ svg wh box title desc bigstr
   where bigstr = unlines [pathToSVG outline strokeWid p | p <- paths]
         title  = "Haskell Logo"
         desc   = "Combination bind and lambda symbol"

svgShapes (w,h) opt = let
   (bounds@((blx,bly),(trx,try)),paths') = shapes opt
   xscale = trx - blx
   yscale = try - bly
   slide (x,y) = (x - blx, y - bly)
   flipy (x,y) = (x, yscale - y)
   scale (x,y) = (w * x / xscale, w * y / xscale)
   trans = scale . flipy . slide

   newbnds@((a,b),(c,d)) = (trans (blx,bly), trans (trx,try))
   box = show a++" "++show b++" "++show c++" "++show d

   paths  = flipPaths trans paths'
   wh     = (show $ round w, show $ round h)  --("8cm", "8cm")
   
   outline = nofill opt

   strokeWid = w/128

   in   pathsToSVG strokeWid wh box outline paths