Prism Maker: five of six loops never closed

DHSeaDev — Chrome Extensions, Windows Tools, & Idle Games

Prism Maker is a page on this site that puts a line of text over a moving backdrop, makes a square card out of it, and lets you leave with the file. It runs in your browser. Nothing uploads, nothing is stored, and there is no account.

The reason it exists: some days the hard part is thinking of the sentence. So the page thinks of one for you, puts it on something worth looking at, and hands it over as a PNG or a three-second MP4 you can keep, set as a lock screen, or post. You press a button and you leave with something. That is the whole job.

I built it in August. This week I went back in and found that most of the videos it had ever made had a visible jump at the loop point.

You can try it at dhseadev.online/prism-maker.

Why generate the lines instead of writing more of them

The deck has 36 lines I wrote by hand. That was the whole feature for a while, and it has a ceiling: you read 36 lines, you start recognising them, and the page stops giving you anything.

So there is a second button, Refract, that builds a line instead of drawing one from the deck. Eight families of phrasing, each with its own sentence templates and its own word pools, and every combination is a hand-written clause joined to another hand-written clause. That comes to 483 distinct lines. I know it is 483 because the set is small enough to print every one and read them all, which I did, and which found six that were broken in ways no automated check had caught.

Refract will also build you a page of five lines at once if you want something longer than a card.

What Refract takes from Morbotron

Morbotron is a Futurama screencap search engine with a meme and GIF generator attached, made by the people behind Frinkiac, the earlier Simpsons one. You search a line, it finds the frame, you leave with a picture.

That shape is what I borrowed: a caption over a picture, made on demand, that you take with you. Not the content and not the code. Just the flow, which those two got right years ago and which I had not seen pointed at something sincere instead of something funny.

Why did the exported videos have a seam

The MP4 export renders 90 frames at 30 frames per second and drives every scene from one number, t, running zero to one. Frame 90 is meant to equal frame 0 so the video loops without a cut.

It did not. I rendered each scene at t=0 and t=1 and diffed the pixels. One scene of the six came back identical. The other five differed, three of them by a maximum channel delta of 229 out of 255. That is not a subtle seam. That is a hard cut, and it was in most of the videos the page had produced.

The cause is small and it reads as correct. A scene positions its elements with something like this:

frac(rnd(i + 5) + t * (0.35 + rnd(i + 12) * 0.4))

Each element drifts at its own speed, somewhere between 0.35 and 0.75 loops per cycle. Those are fractions, so when t reaches 1 the element has finished part of a lap and is not back where it started. The picture at the end of the video is not the picture at the beginning, and the player cuts between them.

The fix is to round every one of those speeds to a whole number, so each element completes an exact count of laps in the time the video runs. Seven scenes now close at a pixel delta of zero.

One took two tries. Shards has a rotation that was not wrapped the same way, and in floating-point arithmetic a value plus three full turns is not bit-for-bit the same number as that value, so it sat one part in 255 off. That is invisible. I fixed it because a check that reports “almost” is a check I will learn to ignore.

The general version, which I am keeping: driving a loop from t = frame / total proves the clock is periodic. It proves nothing about what the scene does with the clock. Assert that the first frame and the last frame are the same pixels.

How the new backdrop stays readable behind text

There is a seventh scene now, called Gallery, and it is the one I nearly got wrong.

The plan was a set of generated backdrops rather than drawn shapes. The first attempt made eight of them, passed every check I wrote, and looked like mud. Grey-brown noise, eight variants that read as about five.

The measurement that explained it: the raw texture has an RMS contrast of 0.15 to 0.31. By the time it is rendered behind the text that falls to 0.005 to 0.012, roughly a thirty-fold collapse, leaving each backdrop about four perceptible steps of brightness for the entire picture. I had spent the whole budget keeping the words readable and left nothing for the image.

The constraint was only ever on brightness. Colour was free and I had not used it. The rebuild drops to four backdrops, built in OKLCH with lightness pinned inside the safe band and hue and saturation carrying the difference instead. The one you get follows the family of the line you just generated, so the picture and the words come from the same place.

Worst measured contrast between text and backdrop, across every palette, ink and placement: 7.94 to 1. The bar is 4.5.

While I was in there I found three text styles on the page sitting at 3.75 to 1, under the accessibility minimum, two of them on 10 and 11 pixel uppercase labels. Those are fixed.

If you want the version of this that sits behind every page on the site instead of inside a card, that is a different canvas and a different post: One canvas, eight scenes, and no libraries. That one is driven by how far you have scrolled. This one is driven by a clock, and you can download the result.

Go make a card: dhseadev.online/prism-maker