I built OohAlive, a page where you draw an arrow on a photo and the picture flows that way. Then you download the loop as a GIF. Three things went wrong along the way and they were more interesting than the shader, so this post is about those.
The shader
Each arrow adds a Gaussian pull to every pixel, based on the distance to the arrow segment. Two copies of the photo slide along that flow half a loop apart and cross fade, so the other copy covers the moment one resets. A freeze brush paints a mask, and where the mask is set the flow is zero. The preview and the export call the same draw function.
Bug one: the loop was twice as long as it needed to be
The two copies swap places after half a cycle, so the picture repeats every half cycle. My first GIF had 40 frames, and frame 20 was identical to frame 0. I was encoding the same second twice. Scaling the phase so 0 to 1 is exactly one repeat halved the frame count.
Bug two: frozen pixels shimmered
I measured a window on the frozen boat across the decoded GIF and found 1,070 changed values out of about 4,600. The raw frames from the shader were identical there, so the encoder was at fault. The palette lookup in gifenc caches by 5-6-5 colour for each call and keeps the match of the first pixel it meets, so the same colour could land on different palette entries in different frames. One lookup table for the whole GIF fixed it, and the same window now shows 0 changed values. With the first fix, the exported file went from 2.1 MB to 887 KiB.
Bug three: a failing test ate 23 GB of memory
A test compared two screenshots with assert.deepEqual. When they differed, Node tried to print both PNG buffers as a diff and the process kept growing until I killed it. Comparing with Buffer.compare and counting draw calls instead of pixels for the pause test fixed it.
What I would like to know
I've only tested Chromium. Firefox, Safari and real phones are untested. It works best on water, clouds and smoke. If a photo looks wrong, I'd like to see it.
Live page: https://arthur031221.github.io/OohAlive/
Source, MIT licensed: https://github.com/Arthur031221/OohAlive

Top comments (2)
The decoded-GIF check is a useful boundary to test, rather than trusting the shader preview. I'd extend it with a thin frozen border around a moving region, including colors near the palette lookup's bucket boundaries. Check that border across every decoded frame, not only a window well inside the frozen area.
For the loop, I'd also compare the last-to-first transition against neighboring frame transitions: frame 20 matching frame 0 proves a repeated period, but doesn't by itself measure whether playback has a visible seam. Keeping those two checks separate would make the palette and phase regressions easier to diagnose. I haven't run OohAlive; these are suggested export fixtures based on the two fixes described here.
thanks, that distinction is useful. I checked the current checks:
verifydecodes the exported GIF and compares its last-to-first step with the typical neighboring step, while a unit test checks every pixel in a synthetic still half across all decoded frames. The fixture does not specifically target a thin frozen border or colors near palette bucket boundaries. I should add that case so palette stability at the moving edge is tested separately from loop continuity.