{"slug": "i-m-building-a-scripting-language-for-whiteboard-animations", "title": "I'm building a scripting language for whiteboard animations", "summary": "A developer built Strokeline, a scripting language for whiteboard animations that lets users describe scenes in text and have them rendered as hand-drawn animations, with scripts simple enough for AI models like ChatGPT or Claude to generate. The project's development was marked by debugging challenges, including a parser error-recovery loop that consumed 4GB of RAM, a React and Zustand state-management cycle causing infinite re-renders, and a bug where typed text vanished from the canvas after rendering.", "body_md": "I started **Strokeline** with one dumb, simple idea:\n\nWhat if you could describe an animation the same way you describe a webpage — with text — and something else just... drew it?\n\nNot AI-generated video.\n\nNot a drag-and-drop editor where you fight with alignment guides for twenty minutes.\n\nA **script**.\n\nYou write:\n\n``` php\nCIRCLE browser\nRECTANGLE server\nARROW browser -> server\n```\n\nHit run, and a hand-drawn whiteboard animation plays.\n\nThe twist — and really the whole point — is that the scripting language is simple enough that you can ask **ChatGPT, Claude, or another AI** to write it for you.\n\nSomething like:\n\n```\nExplain how DNS works in 45 seconds.\n```\n\ngoes in.\n\nA script comes out.\n\nYou paste it in.\n\nYou hit run.\n\nAnd hopefully...\n\n**an animation comes out.**\n\nSimple idea.\n\nTurns out **\"simple idea\" and \"simple to build\" have almost nothing to do with each other.**\n\nSomewhere in week one, my test suite died.\n\nNot failed.\n\n**Died.**\n\nVitest ran out of memory and took the whole process down with it.\n\nIt took a while to track down, but here's what was happening.\n\nMy parser had a recovery mechanism.\n\nIf it hit a broken line, it was supposed to skip ahead to the next recognizable keyword and keep going instead of crashing on the first typo.\n\nGood idea in theory.\n\nThe bug was that one specific keyword — `TEXT`, if you're curious — could be interpreted in two different ways.\n\nIt could be a property:\n\n```\nCREATE server AS RECTANGLE\n  TEXT \"SERVER\"\nEND\n```\n\nBut it was also being treated as a statement boundary that the error-recovery logic could search for.\n\nUnder the wrong conditions, the parser would \"recover\" to a position it had never actually left.\n\nSo it did this:\n\n```\nparse\n  ↓\nerror\n  ↓\nrecover\n  ↓\nsame position\n  ↓\nerror\n  ↓\nrecover\n  ↓\nsame position\n  ↓\n...\n```\n\nForever.\n\nEvery iteration created another diagnostic object.\n\nEventually:\n\n**4GB of RAM.**\n\nThen the OS gave up.\n\nThe actual fix was three lines.\n\nFinding those three lines was not.\n\nOnce the parser was solid, I started building the actual editor.\n\nThe basic idea is pretty simple:\n\n```\n┌─────────────────────┬──────────────────────────┐\n│                     │                          │\n│    SCRIPT EDITOR    │      ANIMATION           │\n│                     │                          │\n│  CREATE browser...  │       ✏️ ○ ───────→ □   │\n│                     │                          │\n│                     │                          │\n└─────────────────────┴──────────────────────────┘\n```\n\nType on the left.\n\nWatch it animate on the right.\n\nI pasted in a two-scene test script to check everything worked end to end.\n\n**Black screen.**\n\nConsole full of red text.\n\n```\nMaximum update depth exceeded.\n```\n\nGreat.\n\nThis one wasn't the parser at all.\n\nIt was React and my state manager, Zustand, getting into a fight neither of them could win.\n\nSomewhere in the code that renders the scene tabs, a piece of derived data — basically:\n\n\"Give me the list of scenes.\"\n\n— was being recalculated on every render.\n\nThat meant it was a new array every time, even when nothing had actually changed.\n\nReact compares objects and arrays by reference.\n\nSo:\n\n```\nnew array\n   ↓\n\"Something changed\"\n   ↓\nre-render\n   ↓\ncalculate scenes\n   ↓\nnew array\n   ↓\n\"Something changed\"\n   ↓\nre-render\n   ↓\n...\n```\n\nIt only showed up with **two or more scenes**.\n\nOne scene?\n\nTotally fine.\n\nTwo scenes?\n\nWelcome to hell.\n\nThat's the kind of bug that makes you paranoid about every other component in your codebase doing the exact same thing quietly, waiting for the right input.\n\nThis one's my favorite because it's so specific that it's almost funny.\n\nI finally got the \"hand-drawn\" reveal working.\n\nText types itself onto the canvas character by character.\n\nShapes trace their own outlines like a pen is drawing them.\n\nIt was genuinely satisfying to watch.\n\nExcept...\n\nA few seconds after any text finished typing, it would just...\n\n**disappear.**\n\nNot fade.\n\nNot glitch.\n\nGone.\n\nLike it was never there.\n\nI had two separate rendering paths.\n\nOne for:\n\n```\nstill animating\n```\n\nwhere the text was drawn live every frame.\n\nAnd another for:\n\n```\nfinished\n```\n\nwhere I could reuse a cached image instead of asking an expensive hand-drawn rendering library to redo the same work sixty times a second for something that wasn't moving.\n\nSmart idea.\n\nBetter performance.\n\nExcept the code calculating how large that cached image needed to be was using a rough guess:\n\n```\ntext.length * fontSize * 0.55\n```\n\nI am not proud of that line.\n\nIt also assumed the text started from a point instead of being centered on that point, which is how it actually gets drawn.\n\nSo everything looked perfectly fine while the animation was running.\n\nThen the animation finished.\n\nThe renderer switched to:\n\n```\n\"just show the cached version\"\n```\n\nAnd suddenly the cached version had the wrong dimensions and the wrong position.\n\nHalf the text disappeared silently.\n\nEvery.\n\nSingle.\n\nTime.\n\nThe animation itself was working.\n\nThe cache was working.\n\nThe transition between the two was the problem.\n\nThat's probably the part I find most interesting.\n\nThere was no mysterious AI behavior.\n\nNo obscure browser API.\n\nNo impossible-to-reproduce hardware issue.\n\nJust normal software bugs.\n\nDifferent bugs.\n\nDifferent systems.\n\nDifferent symptoms.\n\nBut underneath, they all had something in common:\n\n**the program believed something was true when it wasn't.**\n\nThe parser thought it had recovered.\n\nIt hadn't.\n\nReact thought the data had changed.\n\nThe renderer thought the cached text had the correct dimensions.\n\nIt didn't.\n\nThree completely different corners of the codebase.\n\nThree completely different disguises.\n\nSame fundamental problem.\n\nBecause I think there's a specific kind of satisfaction in watching something like this get built in public.\n\nNot just the polished announcement at the end.\n\nThe bugs too.\n\nThe broken builds.\n\nThe weird rendering problems.\n\nThe moments where you stare at a console error wondering how something this stupid could possibly be happening.\n\nThe whole premise of Strokeline is:\n\n```\ntext\n  ↓\nprogram\n  ↓\nanimation\n```\n\nAnd building the thing that makes that possible has turned out to be a pretty good example of exactly that process.\n\nYou write something you believe is correct.\n\nThen reality — the browser, the state manager, the parser, the rendering pipeline — tells you:\n\n\"No.\"\n\nAnd you investigate why.\n\nThe interesting thing about a scripting language for animations isn't really the shapes.\n\nDrawing a circle isn't difficult.\n\nDrawing an arrow isn't difficult.\n\nEven animating them isn't particularly difficult.\n\nThe interesting part is creating a language that is:\n\nFor example, I want something like this to be enough to explain a basic HTTP request:\n\n```\nSCENE 1 \"HTTP Request\"\n\n  CREATE browser AS CIRCLE\n    POSITION 400 500\n    RADIUS 100\n    LABEL \"Browser\"\n    DRAW 1s\n  END\n\n  CREATE server AS RECTANGLE\n    POSITION 1400 500\n    WIDTH 300\n    HEIGHT 220\n    TEXT \"SERVER\"\n    DRAW 1s\n  END\n\n  ARROW browser -> server\n    LABEL \"HTTP Request\"\n    DRAW 1s\n  END\n\nEND SCENE\n```\n\nThat's the direction I'm aiming for.\n\nNot a giant animation framework that requires a 400-page manual.\n\nSomething closer to:\n\n**HTML, but for explaining things visually.**\n\nThis is where Strokeline gets particularly interesting to me.\n\nYou don't necessarily need to learn the scripting language.\n\nYou could simply ask:\n\n```\nExplain how a database index works in 60 seconds.\n\nUse three scenes.\n\nStart with a slow query.\n\nThen show the index.\n\nFinish by comparing indexed and non-indexed lookups.\n```\n\nThe AI generates the script.\n\nStrokeline validates it.\n\nThe renderer executes it.\n\nAnd you get an animation.\n\nThat means the language becomes a kind of **intermediate representation for visual explanations**.\n\nThe AI doesn't have to generate pixels.\n\nIt generates instructions.\n\nThe renderer handles the pixels.\n\nThat's a much more interesting problem to me.\n\nThe core loop works now:\n\n```\nWrite script\n    ↓\nParse script\n    ↓\nBuild scene\n    ↓\nRender animation\n    ↓\nPlay / scrub / inspect\n```\n\nI've also been working on things like:\n\nAnd yes...\n\n**bugs.**\n\nA lot of bugs.\n\nBut that's part of building it.\n\nIf you're the kind of person who reads:\n\n\"AI generates a whiteboard animation script and my platform just... plays it.\"\n\nand immediately thinks:\n\n\"Wait, how does the timeline handle scrubbing backward in the middle of an animation without breaking everything?\"\n\nYou're exactly who I'm building this for.\n\nAnd I'd genuinely love to hear what you'd worry about first.\n\nI'm still figuring out what Strokeline ultimately becomes.\n\nRight now, I'm focused on getting the fundamentals right:\n\n**language → parser → timeline → renderer → animation**\n\nOnce that foundation is solid, there are a lot of directions this could go.\n\nMore animation primitives.\n\nBetter camera control.\n\nReusable components.\n\nTimeline editing.\n\nAI-assisted script generation.\n\nMaybe eventually collaborative editing.\n\nBut for now?\n\nI'm happy that I can type a few lines of text and watch the browser turn them into something that actually moves.\n\nEspecially after spending several hours debugging why the text was disappearing.\n\nMore soon.\n\nProbably with more bugs to tell you about.", "url": "https://wpnews.pro/news/i-m-building-a-scripting-language-for-whiteboard-animations", "canonical_source": "https://dev.to/laakri/im-building-a-scripting-language-for-whiteboard-animations-21gb", "published_at": "2026-09-19 10:24:50+00:00", "updated_at": "2026-09-19 10:54:19.900562+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "generative-ai"], "entities": ["Strokeline", "ChatGPT", "Claude", "React", "Zustand", "Vitest"], "alternates": {"html": "https://wpnews.pro/news/i-m-building-a-scripting-language-for-whiteboard-animations", "markdown": "https://wpnews.pro/news/i-m-building-a-scripting-language-for-whiteboard-animations.md", "text": "https://wpnews.pro/news/i-m-building-a-scripting-language-for-whiteboard-animations.txt", "jsonld": "https://wpnews.pro/news/i-m-building-a-scripting-language-for-whiteboard-animations.jsonld"}}