Squares A and B are the exact same gray. Move the light and the shadow keeps lying.
A checkerboard and a cylinder are ray cast per pixel under a soft sun and sky light. Square A is dark paint in full light and square B is light paint in the cylinder's shadow, and the lighting is chosen so both send exactly the same light to your eye (0.28125 x 1 = 0.75 x 0.375), which lands on the same 8-bit gray. The cylinder's soft shadow is analytic: a capsule swept from the base disc to the top disc's projection, with a penumbra that widens away from the base, so the demo can test every square and relabel A and B whenever the light or cylinder moves. How it works mode splits the picture into reflectance times illumination, the decomposition your visual system performs when it sees B as pale paint in shade.
Try it. Drag from A onto B to lay a bar of the same gray between them, and let go to watch the illusion return. Drag the sun in the sky dial or anywhere on the background to move the light, and drag the cylinder around the board. Mask hides everything but the two squares. Keys: H explains, M masks, Space connects, arrows move the light.
Paste this into Claude Code, Codex or any coding agent to get a simple version running, then take it wherever you like.
Build an interactive version of Adelson's checker shadow illusion with JavaScript and the HTML canvas element. Put everything in a single index.html file with no libraries or build step, so I can open it directly in a browser.
Start simple:
- Make a canvas that fills the window, stays sharp on high-DPI screens (scale by devicePixelRatio), and resizes with the window.
- Draw a 7 x 7 checkerboard in a simple perspective view: project each square's four corners with a pinhole camera (x / z, y / z) and fill the resulting quadrilaterals.
- Light it with arithmetic: dark paint is 0.28125, light paint 0.75, full light 1.0 and shadow 0.375. A dark square in the light (A) and a light square in the shadow (B) both come out to exactly 0.28125, so they get the same gray.
- Draw a cylinder and a soft shadow behind it, and label A and B.
- When I hold the mouse down, draw a thick bar of that same gray from A to B. When I let go, remove it.
Once that works, make it beautiful:
- Ray cast the scene per pixel into an ImageData buffer instead: intersect each camera ray with the floor plane and with the cylinder, and shade with a light direction you can change.
- Compute the cylinder's shadow analytically: from a directional light it is a capsule from the base disc to the top disc's projection on the floor. Use smoothstep on the distance to that capsule for a soft edge that is exactly 0 inside and exactly 1 outside, so A and B stay identical.
- Let me drag to move the light, and relabel A and B whenever the shadow moves.
Explain the key ideas in short code comments. When you're done, tell me how to open it and suggest three directions I could take it next, such as a mask that hides everything except A and B, splitting the image into reflectance and illumination, or rendering the same trick in color.