CrtScreen

A picture shown on a tube rather than on a panel: the glass bows, the phosphors sit in columns and the beam overshoots.

the bar is the only thing moving. turn curve to 0 to see how much of this is the glass

Paste into Claude Code, Cursor or any agent. Pinned to a tag, so it will not change under you.

Options

curve 0.12

0 to 0.5, good: 0.06 to 0.2

How much the glass bows the picture. The one that does the most work: at 0 you have a flat image with stripes on it and nothing below will rescue it, and by 0.35 it is a fish-eye rather than a tube. 0.12 is about a late consumer set, where the curve is obvious at the corners and unremarkable in the middle.

grille 3

0 to 12 px, good: 2 to 5

Width of one phosphor cell in device pixels, so three subpixels fit across it. 0 turns the grille off, which is worth doing once to see how much of the effect is the columns rather than the rows. This is the pitch; `mask` is how strongly it shows. Above about 6 the stripes stop being a texture and become the subject.

mask 0.55

0 to 1, good: 0.4 to 0.7

How strongly the grille shows, from a tint over the picture to the macro view. At 1 the stripes are drawn at full strength, which sends two thirds of every cell to black; at three device pixels a cell is below the size at which a stripe can be drawn cleanly, so on a page that is scaling the canvas what reaches the eye is not a grille but coloured noise. 0.55 is what a tube looks like from the sofa. Set it to 1 once and look at it an inch from the screen.

scanline 0.38

0 to 1, good: 0.2 to 0.5

How dark the gaps between rows go. The number everybody reaches for first and the one that matters least: past about 0.6 the picture is half black and you are looking at a blind.

scanCount 200

60 to 1200, good: 140 to 320

How many lines down the glass. This is a count of lines on a screen, not a frequency in the picture, so it stays put when the image changes. Broadcast was 576 visible lines in Europe and 480 in America; 200 is well under either on purpose, because at a real line count on a dense display the rows fall below a pixel and alias into a moire.

bloom 0.32

0 to 2, good: 0.2 to 0.6

How far a bright area bleeds into its neighbours. Only the parts of the picture above a threshold spill, which is why this reads as a beam overshooting rather than as the whole image going soft.

converge 1.1

0 to 5, good: 0.6 to 1.8

How far red and blue miss each other at the corners, in roughly thousandths of the frame. Small numbers only. Convergence error is something you notice on white type at the edge of the screen and nowhere else, and a set this badly out would have gone back to the shop.

vignette 0.5

0 to 1, good: 0.3 to 0.7

How dim the edges of the tube go. It is doing two jobs: a tube really is dimmer at the edges, and it also hides the corners where the curvature is working hardest and the picture is most stretched.

roll 0.1

0 to 1, good: 0.05 to 0.25

How strongly the bar rolling down the glass shows. 0 removes it and makes the whole item a still, which is the right setting behind text. Keep it low: this is a set very slightly out of step with the mains, not a set that is broken.

brightness 1.05

0.2 to 3, good: 0.9 to 1.4

Applied before the grille and the scanlines take their share, which is why it sits above 1. The rows and the columns together remove roughly a quarter of the light at the default mask, so a picture passed through at 1.0 comes out a little darker than it went in. Raise it with `mask`, not on its own.

contrast 0.12

-0.5 to 1.5, good: 0 to 0.4

Pushed away from the midtone before the grille, for the same reason as brightness. A tube crushes its shadows, and a little of this is what separates the lit parts of the picture from the glow the grille leaves in the black.

tint #ffeedd

css colour

The cast of the tube, multiplied over the picture. Warm on purpose: a phosphor is not neutral, and a tube that renders white as white reads as a flat panel doing an impression. Cool it towards #ddeeff for a monitor rather than a television.

bezel #07070a

css colour

Whatever falls outside the curved glass. Near-black rather than black, so it sits against the page instead of punching a hole in it. If it reads as a visible frame, the curve is too high rather than this being too light.

period 6

1 to 30 s, good: 4 to 12

Seconds for one pass of the bar down the glass. The loop closes exactly on it, so the last frame is the first frame. Longer is calmer and almost always better: at 2 it is a strobe.

maxDpr 2

1 to 3

Device pixel ratio ceiling. The grille is measured in device pixels, so this changes how fine the stripes are as well as what it costs to draw them.

reducedMotionTime 1.5

0 to 30 s

The single frame shown when the user prefers reduced motion. Anything works: the bar is the only thing that moves, and the tube is the point.

What it needs

Category
Surfaces, an effect
Dependencies
None at all
Browser
WEBGL2
Frame budget
~0.3 ms · 1 context
Pinned to
v0.14.0
  • Scanlines drawn over a flat image read as an image with stripes on it. The thing that makes a picture read as a cathode ray tube is a short list that has to be there together, and most of it is not the scanlines
  • The glass is curved. The picture bows outward, straight lines near the edge bend and the corners pull in. Without this nothing else helps: a rectangle with stripes is a rectangle. The bezel is whatever falls outside the bent picture, which is why it is in the shader rather than in CSS
  • The phosphors are in columns. A tube has an aperture grille, red green and blue side by side, which is why a white area in a photograph of a CRT is never white up close. The scanlines are the rows and the grille is the columns, and it is the columns that stop the rows reading as a venetian blind
  • The grille is measured in device pixels rather than CSS ones. It is a physical thing on the glass, so it should get finer on a denser display rather than staying the same size in points
  • The grille is lifted towards white rather than multiplied straight, and by a long way. Multiplied in at full strength it sends two thirds of every cell to black, and at three device pixels a cell is below the size at which a stripe can be drawn cleanly, so on a page scaling the canvas what reaches the eye is coloured noise rather than a grille. `mask` is that lift, and at 1 you get the macro view
  • The beam overshoots, so a bright area bleeds into its neighbours. Three taps across the grille, not a blur: the grille is about to throw away most of the horizontal detail anyway, so a wider and more expensive kernel buys nothing visible
  • The channels do not land on each other. Red and blue are pulled either side of green along the radius, by an amount that grows towards the corners, because that is where the deflection coils are working hardest
  • The scanlines are counted down the glass rather than measured in the picture, so they stay put when the image changes. Fixed to the screen, which is what they are
  • Only the rolling bar moves. Animating the scanlines or the grille turns the whole thing into a pattern sliding over a photograph, which is the usual mistake
  • The <img> stays in the document, hidden. Its alt text is the accessible version of the picture and a page whose script never runs still shows the photograph
  • Five samples of the source per pixel: three for the convergence split and two for the overshoot. Everything else is arithmetic on the result
  • Nothing is allocated per frame and the texture is uploaded once
  • The bar is the only thing that moves, so the item is drawing a near-identical frame most of the time. The runtime stops the loop once the element is off screen, which is where the saving is

Concept credit: The parameter set for a CRT filter, from the Shader Effects library. Curvature is not in theirs and is added here because it is the one that matters most, by Shader Effects Inc.. The implementation here is written from scratch.