diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Chris Done (c) 2018
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Chris Done nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,71 @@
+# sdl2-sprite
+
+Create a horizontal sprite like this:
+
+![](img/blue-fish-sprite.png)
+
+The size of the intended result image is 220x130, so provide that:
+
+    sdl2-sprite blue-fish.png 220x130 --fps 15
+
+This will open a window animating that sprite:
+
+![](img/blue-fish.gif)
+
+(Note: this is a screenflow recording of the animation manually edited
+to repeat, so it's not as smooth as in reality.)
+
+The animation repeats indefinitely at the desired framerate.
+
+## Using Haskell API
+
+Use `load` to load in a sprite for a `Renderer`:
+
+``` haskell
+load :: MonadIO m => Renderer -> FilePath -> V2 CInt -> m Sprite
+```
+
+To render the sprite at a given position use `render`:
+
+``` haskell
+render :: MonadIO m => Sprite -> V2 CInt -> m ()
+```
+
+To advance the sprite's frame to the next frame (or loop), use
+`animate`:
+
+``` haskell
+animate :: Sprite -> Sprite
+```
+
+This is a pure function, so you can use it in a loop. See the source
+in `app/Main.hs` of this repository for an example.
+
+
+## How to make sprites in Inkscape
+
+Choose the size of your intended result e.g. 220x130 and then setup
+the grid in Inkscape to show that size (see the Spacing X/Spacing Y
+fields):
+
+![](img/inkscape-doc-properties.png)
+
+It'll look like this, which is handy:
+
+![](img/inkscape-grid.png)
+
+When you want to make a new frame, use `Ctrl-D` to duplicate the
+selection and use the Transform tool to move it exactly 220 pixels to
+the right and hit Apply.
+
+![](img/inkscape-transform.png)
+
+Make sure that your document dimensions are your width x your frame
+count.
+
+Finally, export your image with the right "Image size" - make sure the
+size is 130 so that the width scales right:
+
+![](img/inkscape-export.png)
+
+Now you're good to go! Hit Export.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ViewPatterns #-}
+-- |
+
+module Main where
+
+import           Control.Concurrent
+import           Control.Monad
+import           Data.List.Split
+import           Data.Monoid
+import qualified Data.Text as T
+import           Options.Applicative.Simple
+import           SDL
+import qualified SDL.Sprite
+import           Text.Read
+
+main :: IO ()
+main = do
+  ((fp, dim, ms), ()) <-
+    simpleOptions
+      "0.0.0"
+      "sdl2-sprite"
+      "Animate sprites with sdl2"
+      ((,,) <$> argument str (metavar "FILE") <*>
+       argument
+         (eitherReader
+            (\i ->
+               case splitOn "x" i of
+                 [w, h] -> V2 <$> readEither w <*> readEither h
+                 _ -> Left "invalid WIDTHxHEIGHT specification"))
+         (metavar "WIDTHxHEIGHT") <*>
+       (fmap
+          (\fps -> 1000 / fps)
+          (option
+             auto
+             (long "fps" <> value (15 :: Double) <> help "Frames per second")) <|>
+        option
+          auto
+          (long "delay" <> value (50 :: Double) <>
+           help "Delay between frames in ms")))
+      empty
+  initializeAll
+  window <-
+    createWindow
+      (T.pack fp)
+      defaultWindow {windowInitialSize = dim, windowHighDPI = True}
+  renderer <- createRenderer window (-1) defaultRenderer
+  -- Resize the window (smaller) if in high DPI (retina) mode.
+  do Just (Rectangle _ (V2 w h)) <- SDL.get (SDL.rendererViewport renderer)
+     let scale =
+           let (V2 w0 h0) = dim
+           in (V2
+                 (fromIntegral w / fromIntegral w0 :: Double)
+                 (fromIntegral h / fromIntegral h0 :: Double))
+     windowSize window $= (fmap round (fmap fromIntegral dim / scale))
+  -- Load up the sprite.
+  sprite0 <- SDL.Sprite.load renderer fp dim
+  -- Render the sprite on a timer.
+  let loop (!sprite) = do
+        events <- pollEvents
+        let eventQuit event =
+              case eventPayload event of
+                KeyboardEvent keyboardEvent ->
+                  keyboardEventKeyMotion keyboardEvent == Pressed &&
+                  keysymKeycode (keyboardEventKeysym keyboardEvent) == KeycodeQ
+                WindowClosedEvent {} -> True
+                _ -> False
+        clear renderer
+        SDL.Sprite.render sprite (V2 0 0)
+        present renderer
+        threadDelay (round (1000 * ms))
+        unless (any eventQuit events) (loop (SDL.Sprite.animate sprite))
+  loop sprite0
diff --git a/sdl2-sprite.cabal b/sdl2-sprite.cabal
new file mode 100644
--- /dev/null
+++ b/sdl2-sprite.cabal
@@ -0,0 +1,36 @@
+name:                sdl2-sprite
+version:             0.0.0
+synopsis:            Sprite previewer/animator
+description:         View your sprites in an animated way with SDL2
+homepage:            https://github.com/chrisdone/sdl2-sprite#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Chris Done
+maintainer:          chrisdone@gmail.com
+copyright:           2015 Chris Done
+category:            Web
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  ghc-options:         -Wall
+  exposed-modules:     SDL.Sprite
+  build-depends:       base >= 4.7 && < 5,
+                       sdl2
+                     , sdl2-image
+  default-language:    Haskell2010
+
+executable sdl2-sprite
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:       base
+                     , sdl2-sprite
+                     , sdl2
+                     , sdl2-image
+                     , optparse-simple
+                     , text
+                     , split
+  default-language:    Haskell2010
diff --git a/src/SDL/Sprite.hs b/src/SDL/Sprite.hs
new file mode 100644
--- /dev/null
+++ b/src/SDL/Sprite.hs
@@ -0,0 +1,71 @@
+-- | Handle sprites in a simple way.
+
+module SDL.Sprite
+  ( load
+  , animate
+  , render
+  , Sprite
+  ) where
+
+import Control.Exception
+import Control.Monad.IO.Class
+import Foreign.C.Types
+import qualified SDL.Image
+import SDL.Vect
+import SDL.Video.Renderer
+
+-- | A loaded sprite.
+data Sprite = Sprite
+  { spriteDimensions :: !(V2 CInt)
+    -- ^ Render dimensions.
+  , spriteTexture :: !Texture
+    -- ^ The texture to render from.
+  , spriteFrame :: !CInt
+    -- ^ The current frame index.
+  , spriteTextureInfo :: !TextureInfo
+    -- ^ Info about the sprite texture.
+  , spriteRenderer :: !Renderer
+    -- ^ The renderer.
+  }
+
+-- | Get the current source rectangle of the sprite's frame.
+spriteRectangle :: Sprite -> Rectangle CInt
+spriteRectangle s = Rectangle (P (V2 x 0)) (V2 w h)
+  where (V2 w h) = spriteDimensions s
+        x = spriteFrame s * w
+
+-- | Load a sprite from file.
+load :: MonadIO m => Renderer -> FilePath -> V2 CInt -> m Sprite
+load ren fp dimensions =
+  liftIO
+    (do texture <- SDL.Image.loadTexture ren fp
+        textInfo <- queryTexture texture
+        evaluate
+          (Sprite
+           { spriteDimensions = dimensions
+           , spriteTexture = texture
+           , spriteFrame = 0
+           , spriteTextureInfo = textInfo
+           , spriteRenderer = ren
+           }))
+
+-- | Advance sprite to the next frame.
+animate :: Sprite -> Sprite
+animate s =
+  s
+  { spriteFrame =
+      if (spriteFrame s + 1) * w < textureWidth (spriteTextureInfo s)
+        then spriteFrame s + 1
+        else 0
+  }
+  where
+    (V2 w _) = spriteDimensions s
+
+-- | Render the sprite at the given coordinates.
+render :: MonadIO m => Sprite -> V2 CInt -> m ()
+render s xy =
+  copy
+    (spriteRenderer s)
+    (spriteTexture s)
+    (Just (spriteRectangle s))
+    (Just (Rectangle (P xy) (spriteDimensions s)))
