After optimizing Revel to handle 1 million cubes, I decided to add animations to make my notes more dynamic. What started as “let’s make shapes move smoothly” spiraled into building an entire reactive UI framework with variables, event handlers, and presentations. Then I realized something wild: the DSL (Domain Specific Language) is Turing-complete. You can implement cellular automata, build interactive quizzes, and create data visualizations, all inside a note-taking app.
This post documents how a simple animation system evolved into something that can theoretically compute anything.
The Animation Foundation
Every complex system needs solid fundamentals. I implemented eight interpolation curves to make animations feel natural:
immediate: Instant jumps (no interpolation)
linear: Constant speed motion
bezier: Smooth professional ease-in-out
ease-in/ease-out: Acceleration/deceleration curves
bounce: Playful bouncing effect at the end
elastic: Spring-like oscillation
back: Overshoots then settles
The syntax follows a consistent pattern: what → where → when → how long → how
Five animation primitives cover everything: animate_move, animate_resize, animate_rotate, animate_color, and animate_appear/animate_disappear for opacity transitions. Multiple animations stack seamlessly. A shape can move, resize, rotate, and change color simultaneously.
Animations can play once (animation_mode) or loop forever (animation_mode cycled). Looping mode is perfect for breathing buttons, rotating logos, or any continuous visualization.
Presentation Mode: Slides and Navigation
Once I had smooth animations, I realized this could be a presentation tool. Why not split scripts into slides?
I added animation_next_slide as a slide delimiter. Press Ctrl+Right Arrow to advance forward, Ctrl+Left Arrow to go back. Each slide starts fresh, previous elements clear automatically, but you maintain forward progress through your narrative.
Each slide can have its own animation mode. You can mix static slides with animated ones, or create continuously looping visualizations. Hide the toolbar with Ctrl+Shift+T for distraction-free presentations.
Variables and Reactivity: Building Interactive Systems
Here’s where it gets interesting. Static presentations are nice, but what about interactive dashboards? What if elements could respond to clicks and update based on calculated values?
I added four variable types with full expression support:
Then came event handlers. This is the magic that makes everything reactive:
The on click handlers fire when elements are clicked. The on variable handlers fire whenever a variable changes. This creates a reactive system where data changes automatically propagate through your UI.
Real Example: Interactive Sales Dashboard
Click a bar → variable updates → animation triggers → total recalculates → label refreshes. Pure reactive data flow with smooth animations.
Interactive Thermostat Control
Every button click updates the temperature, which triggers the gauge to animate smoothly to the new value. The readout updates in real-time. All declarative, all reactive.
Turing Completeness: Rule 110 Cellular Automaton
Here’s the moment I realized this DSL had gone beyond a simple scripting language. I implemented Rule 110, a Turing-complete cellular automaton, in 69 lines of DSL code. When you click START, it dynamically generates 250,000 visual elements (500 cells × 500 generations) in real-time, creating a mesmerizing fractal pattern.
Rule 110 is proven to be capable of universal computation. If you can implement Rule 110, your language can theoretically compute anything. Here’s how it works in Revel:
The Setup
The State Machine
The simulation runs through three phases, orchestrated by the step variable:
Phase 1: Draw (Render the current generation as a row of pixels)
Notice the dynamic element creation: r${gen}c${i} creates unique IDs like r1c0, r1c1, r2c0, etc. The color expression {c[i],c[i],c[i],1.0} maps 0 to black, 1 to white.
Phase 2: Compute (Apply Rule 110’s pattern matching)
The boolean arithmetic trick: (pattern == 1) + (pattern == 2) + ... evaluates each comparison to 0 or 1, summing to 1 if any match. No conditional statements needed!
Phase 3: Copy and Loop (Swap buffers and continue)
The clever continuation: {(gen < 500) * 1} evaluates to 1 (continue) or 0 (stop). When step becomes 1, the whole cycle repeats automatically.
Why This Matters
This demonstrates computational universality. The DSL has:
Arrays for memory storage (int c[500])
Loops for iteration (for i 0 499)
Conditional logic through arithmetic expressions ({(gen < 500) * 1})
Dynamic code generation (r${gen}c${i})
Event-driven execution (on variable step == N)
These primitives combine to create a Turing-complete system. You could implement Conway’s Game of Life, a brainfuck interpreter, or any other computational system.
Grid Visualization: 10,000 Elements in One Click
The grid example showcases the DSL’s ability to generate massive layouts programmatically:
One click creates 10,000 squares with a smooth color gradient. The color calculation ({row/max_index},{col/max_index},0.5,1.0) generates a red-green gradient across the grid. Each cell gets a unique ID like cell0c0, cell42c73, etc.
This demonstrates the power of programmatic generation. Creating complex layouts that would be impossible to build manually.
Global vs. Local Variables: Persistent State
Presentations introduced a challenge: slides clear all elements when you navigate. But what if you need to track quiz scores across multiple questions?
Global variables persist across slides:
This enables multi-slide quizzes, progress tracking, and stateful presentations. Local variables reset per slide, globals persist throughout the entire session.
Interactive Quizzes: Auto-Advancing Slides
Manual slide navigation works for presentations, but quizzes should advance automatically when you answer correctly. Enter presentation_auto_next_if:
The quiz tracks your score globally, provides feedback, and automatically advances when you select the right answer. You can also use text_bind to create text input fields that trigger progression when the user enters the correct string.
The included examples/quiz.dsl has three different question types: multiple choice buttons, text input, and card selection - all with automatic progression and score tracking.
Text Input and Drag-and-Drop
Interactive learning requires more than just buttons. I added two powerful binding mechanisms:
Text Input - Bind a text element to a string variable:
When the user edits the text and presses Enter, the variable updates and triggers all on variable handlers.
Position Tracking - Track element positions for drag-and-drop activities:
Whenever the element moves, the variable updates with the new coordinates as a string like "420,180". Perfect for drag-and-match educational activities.
Data Visualization: Line Plots
Educational content and dashboards need charts. I added a plot shape type that renders line graphs from CSV-style data:
Plots auto-scale to fit the data range, display gridlines with actual values, and include legends for multi-line charts. Perfect for teaching statistics, visualizing experiment results, or creating live data dashboards.
Audio System: MP3 Playlists
Revel already supported images and MP4 videos. Adding MP3 playback was a natural extension:
Audio files are stored directly in the SQLite database as BLOBs, just like images and videos. Connect audio elements with arrows to create playlists. When one track finishes, the next one starts automatically. Connect the last track back to the first for continuous looping.
This enables background music for presentations, audio cues for interactive demos, or educational content with narration.
Type Checker: Catching Errors Before Execution
With great power comes complexity. The DSL grew sophisticated enough that catching errors early became critical. I built a type checker that validates scripts before execution:
Variable type checking (int, real, bool, string)
Array bounds validation
Expression type compatibility
Command syntax validation
Element ID uniqueness
The type checker lives in src/dsl/dsl_type_checker.c and runs automatically when you execute scripts. Instead of cryptic runtime crashes, you get helpful error messages pointing to the exact line and describing what went wrong.
Export to DSL: Reverse Engineering Your Canvas
One of my favorite workflows: build something visually on the canvas, then click “Export to DSL” to generate the code.
I manually drew a triangle with lines and text labels. No code, just visual composition. Then exported it to DSL and got clean, readable code representing every element:
Full triangle.dsl available here
The exported DSL is clean and ready to modify. Duplicate it, parameterize values, or use it as a template. This bridges the gap between visual creation and programmatic generation.
What You Can Build
The combination of infinite canvas, DSL automation, and interactive presentations opens up interesting possibilities:
Interview preparation: Visual algorithm explanations with animations showing how quicksort partitions arrays or how B-trees rebalance
System design practice: Interactive diagrams where clicking a load balancer shows request distribution
Project planning: Timelines with progress tracking and automated status updates
Learning tools: Quizzes that test understanding of distributed systems concepts
You can think visually, prototype interactively, then codify the patterns that work. I’m still exploring what’s possible with these capabilities.
Technical Implementation
The entire feature set is implemented in pure C using:
GTK4 for the UI layer (includes Cairo for 2D rendering)
SQLite3 for database storage (storing scripts, media as BLOBs, element properties)
GStreamer for video/audio playback
No framework bloat. No dependency hell. No bundled JavaScript runtime. Just direct systems programming that compiles to a ~488KB binary and will keep working for decades.
What I Learned
Building this reminded me why I love programming:
- Simple ideas compound. “Let’s add animations” turned into “We built a Turing-complete reactive framework”
- Constraints breed creativity. Working within a note-taking app’s context led to unique solutions
- Composition is powerful. Each feature (variables, events, loops, arrays) multiplied the possibilities
- User-driven design works. Every feature emerged from actual usage: “I wish I could do X” → implement X → discover Y is now possible
The DSL made everything composable. Animations + variables = reactive UIs. Variables + events = interactive quizzes. Loops + arrays + events = cellular automata. Each primitive combines with others to enable emergent complexity.
Try It Yourself
All the examples mentioned are included in the examples/ directory (tested on Linux):
Check out DSL.md for the complete language reference.
I’m Looking for Work
I’m currently looking for a full-time software engineering role. If you’re working on interesting problems and think this kind of work resonates with what your team does, feel free to reach out: [email protected]
Links:
- GitHub: github.com/Dimchikkk/revel
- Complete DSL Reference: DSL.md
- Part 1: Revel: My Experiment in Infinite, Portable Note-Taking
- Part 2: Building a DSL for Canvas Automation in C
- Part 3: Rendering 1 Million Cubes