A Polyspiral is a spiral made of multiple line segments, whereby each segment is larger (or smaller) than the previous one by a given amount. Each segment also changes direction at a given angle.



Task
Animate a series of polyspirals, by drawing a complete spiral then incrementing the angle, and (after clearing the background) drawing the next, and so on. Every spiral will be a frame of the animation. The animation may stop as it goes full circle or continue indefinitely. The given input values may be varied.

If animation is not practical in your programming environment, you may show a single frame instead.



Pseudo code
    set incr to 0.0

    // animation loop
    WHILE true 

        incr = (incr + 0.05) MOD 360
        x = width / 2
        y = height / 2
        length = 5
        angle = incr

        // spiral loop
        FOR 1 TO 150
            drawline
            change direction by angle
            length = length + 3
            angle = (angle + incr) MOD 360
        ENDFOR
    



{-# LANGUAGE OverloadedStrings #-}
import Reflex
import Reflex.Dom
import Reflex.Dom.Time
import Data.Text (Text, pack) 
import Data.Map (Map, fromList)
import Data.Time.Clock (getCurrentTime)
import Control.Monad.Trans (liftIO)
 
type Point = (Float,Float)
type Segment = (Point,Point)
 
main = mainWidget $ do 
 
  -- An event that fires every 0.05 seconds.
  dTick <- tickLossy 0.05 =<< liftIO getCurrentTime 
 
  -- A dynamically updating counter.
  dCounter <- foldDyn (\_ c -> c+1) (0::Int) dTick
 
  let 
      -- A dynamically updating angle.
      dAngle = fmap (\c -> fromIntegral c / 800.0) dCounter
 
      -- A dynamically updating spiral
      dSpiralMap = fmap toSpiralMap dAngle
 
      -- svg parameters
      width = 600
      height = 600
 
      boardAttrs = 
         fromList [ ("width" , pack $ show width)
                  , ("height", pack $ show height)
                  , ("viewBox", pack $ show (-width/2) ++ " " ++ show (-height/2) ++ " " ++ show width ++ " " ++ show height)
                  ]
 
  elAttr "h1" ("style" =: "color:black") $ text "Polyspiral" 
  elAttr "a" ("href" =: "http://rosettacode.org/wiki/Polyspiral#Haskell") $ text "Rosetta Code / Polyspiral / Haskell"
 
  el "br" $ return ()
  elSvgns "svg" (constDyn boardAttrs) (listWithKey dSpiralMap showLine)
 
  return ()
 
-- The svg attributes needed to display a line segment.
lineAttrs :: Segment -> Map Text Text
lineAttrs ((x1,y1), (x2,y2)) =
  fromList [ ( "x1",    pack $ show x1)
           , ( "y1",    pack $ show y1)
           , ( "x2",    pack $ show x2)
           , ( "y2",    pack $ show y2)
           , ( "style", "stroke:blue")
           ]    
 
-- Use svg to display a line segment.
showLine :: MonadWidget t m => Int -> Dynamic t Segment -> m ()
showLine _ dSegment = elSvgns "line" (lineAttrs <$> dSegment) $ return ()
 
-- Given a point and distance/bearing , get the next point
advance :: Float -> (Point, Float, Float) -> (Point, Float, Float)
advance angle ((x,y), len, rot) = 
  let new_x = x + len * cos rot
      new_y = y + len * sin rot
      new_len = len + 3.0 
      new_rot = rot + angle
  in ((new_x, new_y), new_len, new_rot)
 
-- Given an angle, generate a map of segments that form a spiral.
toSpiralMap :: Float -> Map Int ((Float,Float),(Float,Float))
toSpiralMap angle =
      fromList                       -- changes list to map (for listWithKey)
  $   zip [0..]                      -- annotates segments with index
  $   (\pts -> zip pts $ tail pts)   -- from points to line segments
  $   take 80                        -- limit the number of points
  $   (\(pt,_,_) -> pt)              -- cull out the (x,y) values
  <$> iterate (advance angle) ((0, 0), 0, 0)  -- compute the spiral
 
-- Display an element in svg namespace
elSvgns :: MonadWidget t m => Text -> Dynamic t (Map Text Text) -> m a -> m a
elSvgns t m ma = do
    (el, val) <- elDynAttrNS' (Just "http://www.w3.org/2000/svg") t m ma
    return val