<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shyan-Ming Perng</title>
    <description>The latest articles on DEV Community by Shyan-Ming Perng (@smperng).</description>
    <link>https://dev.to/smperng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049937%2Ff6faf3d8-8a6d-403b-9aca-7eb2d084f0c3.jpeg</url>
      <title>DEV Community: Shyan-Ming Perng</title>
      <link>https://dev.to/smperng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smperng"/>
    <language>en</language>
    <item>
      <title>Choreo: an animation DSL whose programs verify their own rendered motion</title>
      <dc:creator>Shyan-Ming Perng</dc:creator>
      <pubDate>Mon, 27 Jul 2026 17:35:01 +0000</pubDate>
      <link>https://dev.to/smperng/choreo-an-animation-dsl-whose-programs-verify-their-own-rendered-motion-21c4</link>
      <guid>https://dev.to/smperng/choreo-an-animation-dsl-whose-programs-verify-their-own-rendered-motion-21c4</guid>
      <description>&lt;p&gt;Most animation tooling assumes a human at the wheel: a timeline, a scrubber, easing curves you nudge by eye. I wanted the opposite — a way to &lt;em&gt;say&lt;/em&gt; what an animation should do and have something check that the pixels actually did it. That's Choreo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one screen
&lt;/h2&gt;

&lt;p&gt;You write a scene:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scene "signup-success" size 800x450 duration 2600ms background #f1f5f9

entities
  card  = rect 320x220 fill #ffffff radius 12
  ring  = circle 30 stroke #16a34a stroke-width 4
  tick  = path "M8 16 L14 22 L24 10" size 32x32 stroke #16a34a stroke-width 4
  label = text "Account created" size 18 weight 600 color #0f172a

layout
  center card
  inside ring card top 36
  center tick in ring
  below label ring gap 18

motion
  seq
    fadeIn card 500ms easeOut
    par
      pop ring 400ms
      drawOn tick 450ms easeOut
    slideIn label from bottom 10 400ms easeOut

checks
  during 0..500   : fadesIn(card)
  during 500..900 : grows(ring) and fadesIn(ring)
  at 1800         : below(label, ring)
  always          : inside(ring, card) and inside(label, card)
  at end          : settled(all)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles to a self-contained SVG + CSS animation. Then the &lt;code&gt;checks&lt;/code&gt; block becomes a spec the verifier runs against the &lt;em&gt;rendered&lt;/em&gt; result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx choreoc examples/signup-success.choreo &lt;span class="nt"&gt;-o&lt;/span&gt; out &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;span class="c"&gt;#  → out/signup-success.html      the animation&lt;/span&gt;
&lt;span class="c"&gt;#  → out/signup-success.spec.mjs  the checks, as a verifier spec&lt;/span&gt;
&lt;span class="c"&gt;#  → 19/19 checks passed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not just screenshot-diff it?
&lt;/h2&gt;

&lt;p&gt;Visual-regression tools (Chromatic, Percy, Applitools) compare frames. They're great at "this looks different from last time" and useless at "the toast slid in &lt;em&gt;before&lt;/em&gt; it faded and had settled by 1.2s." Choreo's verifier samples the real trace — element positions, opacity, size over time, driven by Playwright — and asserts first-order predicates over it: &lt;code&gt;movesLeft&lt;/code&gt;, &lt;code&gt;fadesIn&lt;/code&gt;, &lt;code&gt;inside(a, b)&lt;/code&gt;, &lt;code&gt;settled(all)&lt;/code&gt;, with time windows (&lt;code&gt;during&lt;/code&gt;, &lt;code&gt;at&lt;/code&gt;, &lt;code&gt;always&lt;/code&gt;, &lt;code&gt;at end&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The verifier is a separate package and works on &lt;strong&gt;any&lt;/strong&gt; web animation, not just Choreo output — CSS keyframes, GSAP, Motion, a Remotion export. If you have an animation and a claim about how it should move, you can check the claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup-flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file:///…/signup-flow.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#toast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#msg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;during&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;movesLeft(toast)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fadesIn(toast)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible(toast)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inside(msg, toast)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;atEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fadedOut(toast)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this came from, and what it isn't
&lt;/h2&gt;

&lt;p&gt;It began as an internal probe: can an LLM author animation reliably? On its own, not really — roughly half right. The research I leaned on (MoVer, SIGGRAPH 2025) shows that wrapping generation in a formal verify-and-correct loop closes most of that gap. Choreo is that shape made concrete: a grammar small enough for a model to hit from a spec, and a checker that catches it when it's wrong.&lt;/p&gt;

&lt;p&gt;It is a research artifact, released as-is under MIT. No roadmap promise, no support SLA. There's a cross-framework benchmark in the repo if you want to poke at whether the DSL actually beats raw CSS (short version: the durable win is authoring cost, not correctness). Issues and PRs welcome; treat it as a foundation, not a maintained service.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/JieGouAI/choreo" rel="noopener noreferrer"&gt;https://github.com/JieGouAI/choreo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm i @choreo-oss/lang @choreo-oss/verify&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: this came out of JieGou, an ops-automation company I'm building; Choreo is an open side-output, not a product we sell.&lt;/p&gt;

</description>
      <category>svg</category>
      <category>css</category>
      <category>animation</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
