Skip to content

Commit

Permalink
feat: Add support for creating simple 2D animations
Browse files Browse the repository at this point in the history
We have a new ADT, `Picture`, representing 2D images, and a primitive function `animate : Integer → (Integer → Picture) → Animation`, where `Animation` is a new primitive type, currently implemented as a base-64 encoding of a GIF for ease of use by clients. The arguments are a duration, in seconds, and a description of how to render each frame. This is inspired by the API of pure functional animation libraries such as Haskell's Gloss. We opt for keeping the API simple for now rather than providing full flexibility, so animations are always 10 frames/s on a low-resolution widescreen grid, looping forever (see `Values which are hardcoded`... comment).

Both implementation and student-facing API are really just a proof-of-concept for exploring the use of Primer for effectful programming. Though much of what is added here (`Picture` ADT, new primitives etc.) is likely to remain useful for any future work in this direction.

The one major current limitation is around the kinds of functions which we can pass as the second argument to `animate` without the evaluator getting stuck (`simple substitution hack`... comment). This is a high-priority task which will require some reworking of the evaluator, the details of which are as yet unclear.

Using GIFs with `tasty-golden` for testing mostly works well, except that it has some issues displaying the `ByteString` in the terminal when the output differs from what is expected (outputs `Exception: Cannot decode byte`). But this is fine, since what we'd want to do in such a situation is to just pass `--accept` and compare the old and new GIF as rendered.

Signed-off-by: George Thomas <[email protected]>
  • Loading branch information
georgefst committed Nov 13, 2023
1 parent a482844 commit c4b1393
Show file tree
Hide file tree
Showing 17 changed files with 591 additions and 19 deletions.
2 changes: 2 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@
# https://github.com/input-output-hk/haskell.nix/issues/1242
packages.mtl-compat.writeHieFiles = false;
packages.bytestring-builder.writeHieFiles = false;
packages.fail.writeHieFiles = false;
packages.diagrams.writeHieFiles = false;
}
{
#TODO This shouldn't be necessary - see the commented-out `build-tool-depends` in primer.cabal.
Expand Down
18 changes: 18 additions & 0 deletions primer-service/test/outputs/OpenAPI/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,24 @@
"contents"
],
"type": "object"
},
{
"properties": {
"contents": {
"type": "string"
},
"tag": {
"enum": [
"PrimAnimation"
],
"type": "string"
}
},
"required": [
"tag",
"contents"
],
"type": "object"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions primer/gen/Primer/Gen/Core/Raw.hs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ genPrimCon =
_ = \case
PrimChar _ -> ()
PrimInt _ -> ()
PrimAnimation _ -> ()

genType :: ExprGen Type
genType =
Expand Down
3 changes: 2 additions & 1 deletion primer/gen/Primer/Gen/Core/Typed.hs
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ genChk ty = do
brs0 <- Gen.list (Range.linear 0 5) $ do
p <- pg
(p,) . CaseBranch (PatPrim p) [] <$> genChk ty
let brs = nubSortOn ((\case PrimInt n -> Left n; PrimChar c -> Right c) . fst) brs0
let brs = nubSortOn ((\case PrimInt n -> Left (Left n); PrimChar c -> Left (Right c); PrimAnimation b -> Right b) . fst) brs0
fb <- genChk ty
pure $ Case () e (snd <$> brs) (CaseFallback fb)

Expand Down Expand Up @@ -676,6 +676,7 @@ genPrimCon = catMaybes <$> sequence [whenInScope PrimChar 'a' genChar, whenInSco
_ = \case
PrimChar _ -> ()
PrimInt _ -> ()
PrimAnimation _ -> ()

-- We bias the distribution towards a small set, to make it more likely we
-- generate name clashes on occasion
Expand Down
4 changes: 4 additions & 0 deletions primer/primer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ library
Primer.App.Utils
Primer.Builtins
Primer.Builtins.DSL
Primer.Builtins.Picture
Primer.Core
Primer.Core.DSL
Primer.Core.Meta
Expand Down Expand Up @@ -109,8 +110,11 @@ library
, aeson >=2.0 && <2.2
, assoc ^>=1.1
, base >=4.12 && <4.19
, base64-bytestring ^>=1.2.1
, containers >=0.6.0.1 && <0.7.0
, deriving-aeson >=0.2 && <0.3.0
, diagrams-lib ^>=1.4.6
, diagrams-rasterific ^>=1.4.2
, exceptions >=0.10.4 && <0.11.0
, extra >=1.7.10 && <1.8.0
, generic-optics >=2.0 && <2.3.0
Expand Down
1 change: 1 addition & 0 deletions primer/src/Primer/Builtins/Picture.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module Primer.Builtins.Picture () where
2 changes: 2 additions & 0 deletions primer/src/Primer/Core/Meta.hs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ instance HasMetadata (Meta a) where
data PrimCon
= PrimChar Char
| PrimInt Integer
| -- | Contains a base-64 encoding of an animated GIF.
PrimAnimation Text
deriving stock (Eq, Show, Read, Data, Generic)
deriving (FromJSON, ToJSON) via PrimerJSON PrimCon
deriving anyclass (NFData)
Expand Down
5 changes: 3 additions & 2 deletions primer/src/Primer/Module.hs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import Primer.JSON (
ToJSON,
)
import Primer.Name (Name)
import Primer.Primitives (allPrimTypeDefs, primDefName, primitiveModuleName)
import Primer.Primitives (allPrimTypeDefs, pictureDef, primDefName, primitiveModuleName, tPicture)
import Primer.TypeDef (TypeDef (..), TypeDefMap, forgetTypeDefMetadata, generateTypeDefIDs)

data Module = Module
Expand Down Expand Up @@ -133,10 +133,11 @@ nextModuleID m =
primitiveModule :: MonadFresh ID m => m Module
primitiveModule = do
allPrimTypeDefs' <- traverse (generateTypeDefIDs . TypeDefPrim) allPrimTypeDefs
pictureDef' <- generateTypeDefIDs $ TypeDefAST pictureDef
pure
Module
{ moduleName = primitiveModuleName
, moduleTypes = M.mapKeys baseName allPrimTypeDefs'
, moduleTypes = M.mapKeys baseName allPrimTypeDefs' <> M.fromList [(baseName tPicture, pictureDef')]
, moduleDefs = M.fromList $ [(primDefName def, DefPrim def) | def <- enumerate]
}

Expand Down
1 change: 1 addition & 0 deletions primer/src/Primer/Pretty.hs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ prettyExpr opts = \case
prim = \case
PrimChar c -> "Char" <+> pretty @Text (show c)
PrimInt n -> "Int" <+> pretty @Text (show n)
PrimAnimation n -> pretty @Text (show n)
typeann e t = brac Round Yellow (pE e) <+> col Yellow "::" <> line <> brac Round Yellow (pT t)

-- When grouped: " x "
Expand Down
Loading

0 comments on commit c4b1393

Please sign in to comment.