Meme as Code. A programming language where memes are first-class citizens.
@two_panel {
top: "Me: it works on my machine"
bottom: "Prod: lol no"
} |> glitch => "meme.png"
curl -fsSL https://macstudio.meme/install.sh | bashOr build from source:
cmake -S . -B build && cmake --build buildEvery meme expression follows a composable grammar. Here's how all the pieces fit together:
@<template> [WxH] [style] { top: "..." bottom: "..." } [|> effect] [=> "path"]
@<template> [WxH] [style] "one-liner text" [|> effect] [=> "path"]
@"path/to/image.png" [WxH] [style] { ... } [|> effect] [=> "path"]
@blank "Hello" // minimal
@two_panel { top: "A" bottom: "B" } // block with positions
@square 800x800 { top: "Sized" } // with dimensions
@dark neon { top: "Styled" } // with style
@dark 800x800 neon { top: "All" } |> glitch // size + style + effect
@"photo.png" { top: "Custom" } => "out.png" // custom template + save
effect <name> = <effect> >> <effect> >> ... ;
effect glitch = pixelate(4) >> contrast(1.8) >> noise(0.2);
effect vintage = sepia >> brightness(0.9);
effect cyberpunk = hueShift(180) >> contrast(1.5) >> chromatic(3) >> glow(4);
style <name> {
color: "#RRGGBB"
outline: <pixels>
outlineColor: "#RRGGBB"
shadow: <offset>
shadowColor: "#RRGGBBAA"
}
style neon {
color: "#00FF41"
outline: 4
outlineColor: "#003300"
shadow: 3
shadowColor: "#00FF4180"
}
gif [loop] {
<meme> [|> effect] : <duration> [sound <name-or-path>]
--- <transition> <duration> [easing] [sound <name-or-path>] ---
<meme> [|> effect] : <duration> [sound <name-or-path>]
...
} [=> "path"]
gif loop {
@blank "3" : 500ms
@blank "2" : 500ms
@blank "GO!" : 1s
} => "countdown.gif"
Add --- transition --- separators between entries for a cross-faded sequence:
gif loop {
@two_panel { top: "Act 1" } |> vintage : 3s
--- crossfade 300ms ease ---
@two_panel { top: "Act 2" } |> glitch : 3s
--- fadeBlack 400ms ---
@blank "fin." : 2s
} => "story.gif"
Save with .mp4 for H.264 video (requires ffmpeg on your PATH). Sound cues
only reach the output in an MP4, since GIF cannot carry audio - saving a
cue-bearing animation to .gif warns and drops the cues.
gif {
@dark "step one" : 2s sound ding
--- slideLeft 300ms easeOut sound whoosh ---
@dark "narrated" : sound narration
@dark "done" : 2s sound chime
} => "steps.mp4"
A duration of sound <name> holds the frame for the bite's own length (clamped
to 10s); the same works on a transition. Bites resolve as a built-in name
(ding), a dotted name (acme.jingle from assets/sounds/acme/jingle.*), or a
quoted script-relative path (sound "audio/x.wav").
An edit block is a gif block that also takes video footage. Clips are
sampled at 30fps, trimmed with start..end, captioned onto every frame, and
mixed freely with still frames and transitions. A clip's own audio comes along
automatically. Requires ffmpeg on your PATH.
edit loop {
caption "three weeks earlier" : 1s
--- fadeBlack 400ms ---
clip "footage.mp4" trim 2s..8s caption "when the build goes green"
--- crossfade 300ms ---
@dark "the end" : 1s
} => "reaction.mp4"
A clip has no : duration - its length comes from the footage. A caption with
no clip before it is a text card on a blank background. Clips sample at full
frame rate, capped at 9000 source frames (five minutes), and going over errors
rather than truncating. Everything is re-encoded to H.264 once, and footage above
30fps loses frames.
grid <cols>x<rows> {
<meme> [|> effect]
<meme> [|> effect]
...
} [|> pad(n)] [|> border(n)] [=> "path"]
grid 2x2 {
@blank "A" |> sepia
@blank "B" |> glitch
@blank "C" |> vintage
@blank "D" |> deepfry
} |> pad(5) |> border(2) => "grid.png"
beside(<meme>, <meme>) // side by side
stack(<meme>, <meme>) // vertical
<expr> |> pad(<px>) // padding
<expr> |> border(<px>) // border
<expr> |> <fn> // pass as first arg
<expr> |> <fn>(args) |> <fn> ... // chain
<fn> >> <fn> >> <fn> // compose into new function
<array> |> map(<fn>) // transform each
<array> |> filter(<fn>) // keep matches
<array> |> reduce(<fn>, <init>) // fold to value
<array> |> zip(<array>) // pair elements
["Mon", "Tue", "Wed"]
|> map(d -> @two_panel { top: d })
|> reduce((g, m) -> g.frame(m, Duration(500)), Gif())
|> save("week.gif")
<expr> => "filename.png" // save to ~/mac/output/
<expr> => "filename.gif" // auto-detects format
<expr> => "subdir/file.png" // explicit relative path
<expr> => "/tmp/file.png" // explicit absolute path
If MAC_OUTPUT_DIR is set, saves are redirected there and only the filename portion is kept.
| Element | Options |
|---|---|
| Templates | two_panel three_panel bottom_text blank dark wide tall square four_panel caption_bar or @"path" |
| Positions | top bottom center |
| Effects (param) | blur(n) pixelate(n) noise(n) saturate(n) contrast(n) brightness(n) jpeg(n) hueShift(n) glow(n) posterize(n) chromatic(n) threshold(n) tint(n) |
| Effects (direct) | invert sepia sharpen vignette grayscale deepfry |
| Transitions | crossfade slideLeft slideRight slideUp slideDown wipe fadeBlack zoom |
| Easing | linear ease easeIn easeOut easeInOut bounce |
| Durations | 400ms 2s Duration(400) or sound <bite> (the bite's own length) |
| Sizes | 800x600 Size(800, 600) |
| Sound bites | whoosh ding chime riser boom record_scratch click error or acme.jingle / "audio/x.wav" |
| Output formats | .png .gif .mp4 (mp4 needs ffmpeg; only mp4 carries sound) |
| Clip sources | .mp4 .mov .m4v .webm .mkv .avi (needs ffmpeg; sampled at 30fps) |
Mac is a dynamically typed language with functions, closures, classes, arrays, maps, arrow functions, pipes, and operator overloading.
See LANGUAGE.md for the full reference.
Syntax highlighting, LSP (hover, go-to-definition, inlay hints, semantic tokens, signature help, find references, document symbols), snippets, and file icons.
cd mac-lang && npm install && npx tsc && cd ..
ln -sf "$(pwd)/mac-lang" ~/.vscode/extensions/mac-langcmake --build build && bash tests/run_tests.shSee docs/BUILDING.md and docs/ARCHITECTURE.md.
MIT