Visualisere

torsdag 31. desember 2009 18:26

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 List (intersperse)
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)

unspaces pieces = concat $ intersperse " " pieces

-- 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 = unspaces [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 

Recent content is available under a simple permissive license.

Sist oppdatert søndag 18. juli 2010 22:53
 

Legg til kommentar


Sikkerhetskode
Vis ny kode

Samarbeidspartner

Interaktiv 3D logo

Logg inn


Visualisere