Four actual frames from the Petroly ad side by side: opening, price alert, price comparison and closing, in English with euro prices.
Back to Blog

Blog

A Video Written in Code

The most unexpected unit test I have seen from my AI agents today is a check that a push notification in a video cannot appear before the price in its animated chart reaches its target.

A unit test. For a video. 🤯

I needed a 15-second promotional video for Petroly, my fuel-price app. Claude Code went ahead and wrote a JavaScript renderer that reuses my app’s design. I could then describe a change in plain language, watch the result and refine it.

I never opened a video editor.

The chart, the notification, the sorting animation and the transitions all live in the source. I can change them, review a diff and render again. The more we worked on it, the more possibilities I saw: tests for the timing, translations from the app, even a video with transparent corners.

A video. Written in code.

Run the renderer yourself

Before watching the finished video, try dragging through it.

What you see below is not a video file. Your browser runs the same renderer core that produces the video. Every position of the slider asks it for a new SVG frame. Change the language, jump to a scene, or open the SVG to see what the code produced.

Drag through time

Loading frame…25 frames per second
    Show the SVG for this frame

    No pre-rendered images. The chart, the notification, the sorting list and the QR code are computed for the exact moment you pick. Pick a language and the copy, currency, number format and QR destination change with it.

    The video is a function of time

    The idea at the heart of it is small:

    frame(timeInSeconds) // → an SVG string for that exact moment

    At two seconds, the function draws the opening. Later, it draws the price chart crossing the target and the notification appearing. Later still, the station list sorts itself. Every position, fade and bounce is a little bit of arithmetic on t.

    To turn that into a video, the renderer asks for 375 frames, 25 per second for 15 seconds, and streams them into FFmpeg. With the process handling shortened to writeToFfmpeg, the loop looks like this:

    for (let i = 0; i < DURATION * fps; i++) {
      const pixels = await sharp(Buffer.from(frame(i / fps)))
        .ensureAlpha()
        .raw()
        .toBuffer();
      await writeToFfmpeg(pixels);
    }

    Time → SVG → RGBA pixels → FFmpeg → video. sharp turns each SVG into pixels. FFmpeg encodes the frames and adds the soundtrack.

    If the notification should arrive later, that is a number in the source. If I want to inspect one moment, I can render that exact frame. With the same inputs, the renderer produces the same sequence of frames.

    The creative part was a conversation

    The first render gave me something to react to. From there, the process felt surprisingly familiar: look at the result, describe what should change, and review the next version.

    Some requests were tiny: keep the tap on the sorting control visible a little longer, move it slightly to the left, remove an extra border. Others changed how a whole scene worked.

    Show the choice clearly. A station comparison became more useful than a generic cheapest-time chart. Street names added little to the demo and were hard to read. The rows now use the app’s own price categories and colours, from “very cheap” to “very expensive”.

    Give the movement a reason. A visible tap changes sorting from distance to price. The other rows fade back while the cheapest station moves to the top, and the checkmark appears after the rows settle. You can follow what happened and why.

    Each request became a code change and a new preview. I could compare versions side by side and keep the parts I liked. Git could preserve the renderer and its previews together.

    The agent handled the implementation. I still had to watch and judge it. Several technically correct versions felt too busy. That part needed my eyes and my taste.

    My agent wrote unit tests for a video

    This is the part that caught me off guard.

    In the price-alert scene, the push notification must not appear before the plotted price reaches its target. The agent turned that sequence into a test. With setup and imports omitted, the idea fits in a few lines:

    test('the alert follows the price reaching its target', () => {
      const scene = SCENES.find((scene) => scene.kind === 'alert');
      const targetReachedAt = scene.from + ALERT_AT;
    
      const before = frame(targetReachedAt - 0.1);
      const after = frame(targetReachedAt + 0.1);
    
      assert.doesNotMatch(before, /id="push-notification"/);
      assert.match(after, /id="push-notification"/);
    });

    The renderer calculates ALERT_AT from the plotted prices and the chart’s animation timing. Adding the scene’s start gives us the moment the target is reached. Just before it, no notification. Just after it, a notification. Automated tests check that this sequence still works after every edit. I don’t even have to replay the whole video to verify the timing as I refine it.

    Other tests check the QR code’s layout and quiet zone, that the list starts sorted by distance and ends sorted by price, and that none of the 375 SVG frames contains NaN, Infinity or undefined.

    There is even a check that the cheapest station’s highlight waits until all the rows have finished moving. That is a design decision expressed as an assertion. The same tools I use to protect an app’s behaviour can protect the behaviour of its video.

    There are checks on the rendered pixels too. One makes sure the logo rasterises, rather than merely checking that an image element exists in the SVG.

    I would not have thought to ask for any of this when I asked for a promotional video. Seeing the agent treat it as testable software was the surprise.

    Tests protect the mechanics. Watching protects the feel. I want both.

    My other discovery: video can be transparent

    I knew PNGs could have transparent pixels. I had never thought about video doing the same.

    I wanted the video to look like my iPhone’s display, rounded corners included. An alpha channel makes the area outside those corners transparent, so the video blends into any background.

    Try changing the background behind this frame:

    Change what is behind the frame
    Rendered live in English with euro prices. The SVG's transparent corners reveal the background, without CSS corner clipping. Choose “Phone frame” to see it inside an iPhone-style outline.

    The trick is preserving that transparency all the way through the export. The editing master is ProRes 4444 with alpha in a MOV container. A .mov filename alone does not guarantee transparency; the codec and pixel format have to support it too.

    The frame above is an SVG generated live by the renderer. The browser MP4s below have a background composited underneath them. Their checkerboard is part of the picture; the editing master keeps the actual alpha channel.

    The rounded rectangle was already a shape in the SVG. Preserving the surrounding transparent pixels turned it into a video I could place over another scene. I had no idea I could do that with the same code.

    Twelve localized versions, one animation

    Once I liked the German version, translation became another input to the renderer. The twelve versions cover ten languages, with English in euro and in British pounds, and separate European and Brazilian Portuguese variants. The scenes and timing are shared:

    • Local formatting. The British version shows pence per litre and distances in miles, the Danish one kroner, the Brazilian one reais. The illustrative prices keep their order and cross the target at the same moment. They are not live quotes or currency conversions.
    • The app’s own words. “Price alert” and the five price categories come straight from the app’s translations, so the ad speaks the same language as the app it advertises.
    • Localized links. Each version chooses the appropriate QR destination, using the English page where a translated landing page is unavailable. The codes keep the same size and position in the animation.
    • Text that fits. Every string is measured with the real font and shrinks only where it has to. “Très bon marché” is a lot longer than “Very cheap”.

    Translations still need a human read, and the layouts still need a visual review. But if I improve the sorting animation, every version can inherit the change on its next render. I do not have twelve separate timelines to keep in sync.

    Here are the rendered videos with their soundtrack. Pick a language and press play:

    The result, in 12 localized versions

    Same code. A different audience.

    A 15-second Petroly ad, rendered locally from JavaScript, SVG and a soundtrack.

    640 Ă— 1,080 pixels
    25 frames per second
    375 frames, one renderer

    Read the video description

    The Petroly logo introduces cheaper fuel. A Diesel chart falls to a target price, then a price-alert notification appears. Five colour-coded station categories, from very cheap to very expensive, start sorted by distance. A tap changes the sort order to price. The cheapest station moves to the top and gets a checkmark. The closing screen says the app is free and available for iPhone and Android. A QR code stays fixed throughout. The only audio is instrumental music.

    No video editor. No editing-software subscription.

    The rendering stack is JavaScript, SVG, sharp and FFmpeg. The rendering tools are open source. The soundtrack has a free license as well.

    I already use coding agents to build my apps, as I described in my post on agentic software development. This project broadened my idea of what I can build with code. I didn’t even need to buy a video editor or pay for another subscription.

    I can now review a video change as a diff, ask a test whether a notification arrives too early, render one exact frame to check a layout, and produce another language from the same animation.

    I can describe what I want to see, watch it take shape, and keep improving it through code. I am still excited that a video can work this way.

      Type to search blog posts and products…