Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 43 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,129 +1,69 @@
# Tententoon
<h1 align="center">tententoon</h1>

> *Een prentententoonstelling.* — M. C. Escher, 1956
<p align="center"><em>noun</em>&nbsp;&nbsp;·&nbsp;&nbsp;a self-repeating image whose copies spiral as they shrink — in the manner of M.&nbsp;C.&nbsp;Escher's <em>Print&nbsp;Gallery</em> (1956).</p>

A browser-native Droste machine. Drop in any photograph, frame the rectangle that should
contain a smaller copy of itself, and watch the image recurse into its own infinity — the
way the boy in Escher's *Print Gallery* stares into the picture he is standing inside of.

Everything runs locally. No uploads, no backend, no account. Just the canvas, the maths,
and your image.

---

## What it does

- **Drop or pick** any image — JPEG, PNG, anything the browser can decode.
- **Frame** a rectangle inside it. That rectangle becomes the next iteration of itself.
- **Zoom forever** — a continuous, seamless loop into (or out of) the nested copy.
- **Export** the result as a still PNG, a looping MP4, or a GIF. All produced in-browser.
- **Tweak** the spiral: pick the centre, the scale ratio, the rotation, the depth.
- **Two views**: a polished editor (top bar, tool rail, canvas, inspector, timeline) and a
log-domain explorable for those who like to see the conformal map at work.

The geometry is the same one Escher's mathematicians used to "complete" the Print Gallery:
a log-polar warp that turns nested similarity into a straight line. We let you see both
sides of that mirror.
<p align="center">
from the Dutch title <em>Pren<b>tententoon</b>stelling</em> — <em>prenten</em> (prints) + <em>tentoonstelling</em> (exhibition),<br>
with the coined word sitting right where the two halves meet.<br>
A browser tool for making the same move with your own images.
</p>

---

## Why "Tententoon"?
## Same picture, two infinities

A contraction of *prentententoonstelling* — the Dutch word for "print gallery" — squeezed
the way Escher squeezed his canvas. Short enough to type, long enough to remember.
<table>
<tr>
<td align="center" width="50%"><img src="public/Droste_1260359-nevit.jpg" width="360" alt="A photograph of a person holding a frame; inside the frame is the same person holding the same frame, repeating into the distance."></td>
<td align="center" width="50%"><img src="public/tententoon-demo.gif" width="360" alt="The same photograph, with the nested copies rotating as they shrink, winding the whole image into a smooth spiral."></td>
</tr>
<tr>
<td align="center"><strong>Droste</strong> — a picture inside itself, dropping <em>straight down</em> forever.<br><sub>(this one is a real photograph)</sub></td>
<td align="center"><strong>tententoon</strong> — the same recursion, <em>bent into a spiral</em> that still closes seamlessly.</td>
</tr>
</table>

---
> Same rule — a copy inside a copy inside a copy. One drops straight down; the other takes the scenic route, and still arrives on time.

## Run it locally
## The idea

```sh
bun install # or: npm install
bun run dev # or: npm run dev
```
Nest a picture inside itself and you get the **Droste effect**: each copy sits squarely inside the last, shrinking by the same step, forever.

Then open the URL Vite prints. The editor lives at `/`; the log-domain explorable lives
at `/ui1`.
A **tententoon** asks one stranger question — *what if each copy also turns as it shrinks?* The nested frames then wind into a spiral. Straight lines bow into curves, the whole picture twists, and yet nothing tears: follow any line inward and it meets itself exactly.

### Build
The trick is a single move — **take the logarithm.** Shrinking-and-repeating is multiplication, and logarithms turn multiplication into addition, so "shrink, then repeat" becomes "slide over, then repeat" — a plain, evenly-spaced grid. Add rotation and that grid simply *tilts*; roll it back up and the tilt is a spiral. The loop is seamless because, in log-space, one full turn is just a shift by exactly one step — landing you on an identical picture.

```sh
bun run build # → dist/
bun run preview # serve dist/ locally
bun run smoke # headless smoke test
```
**→ The whole story — why it spirals, and why it never tears:** [silvio-tententoon.pgs.sh/explain.html](https://silvio-tententoon.pgs.sh/explain.html)

---
## Where the name comes from

## How it works, briefly
Escher drew *Print Gallery* by hand in 1956 and left a white blot at its centre — the spiral grew too tight to finish. In 2003, **Hendrik Lenstra** and **Bart de Smit** found the exact map behind it: the idealised picture contains a copy of itself rotated **157.6256°** and shrunk by **22.5837×**. With those two numbers, they completed the picture by computer. In 2026, **3Blue1Brown** turned the whole argument into a [beautifully animated video](https://www.youtube.com/watch?v=ldxFjLJ3rVY).

The Droste effect is a single operation applied to itself: take the outer image rectangle,
map it onto the user's inner rectangle, paint, repeat. After a few iterations the inner
copy is sub-pixel and we stop.
## Make one

For the smooth infinite zoom, the inner rectangle's *scale ratio* `S` and rotation angle
`θ` define a logarithmic spiral. Mapping image coordinates through
`log(z - c) → log(z - c) + log(S) + iθ` turns that spiral into a translation in
log-space — which is why the loop is exactly seamless: translating by one full period
lands on the same picture.
Drop in any photo, draw the rectangle where the next copy should sit, and flip between the two infinities — the straight Droste fall or the tententoon spiral. Export the loop as a PNG, GIF, or video. Everything runs in your browser: no upload, no account, no server.

The renderer is WebGL via [twgl.js](https://twgljs.org/), with a CPU fallback worker for
environments without a usable GL context. Supersampling adapts to the local Jacobian so
the spiral's tight centre stays sharp without paying for it across the whole frame.
**Live:** [silvio-tententoon.pgs.sh](https://silvio-tententoon.pgs.sh/)

---

## Tech
<details>
<summary><strong>Run it locally</strong></summary>

| Layer | Choice |
|-------------|--------|
| UI | Svelte 5 (runes) + Vite |
| Rendering | WebGL (twgl.js), with worker-driven CPU fallback |
| State | `$state` runes, plain CSS custom properties for theming |
| Exports | `MediaRecorder` (MP4/WebM), `gifenc` (GIF), `<canvas>.toBlob` (PNG) |
| Storage | `localStorage` for the last session's selection |
| Backend | none — by design |

---

## Layout

```
src/
App.svelte root: routing, bootstrap, panels
components/
EscherPanel.svelte static Droste render
EscherZoomPanel.svelte animated spiral zoom (GPU + CPU fallback)
LogPanel.svelte log-domain view
SourcePanel.svelte the source-image picker with nested rect
UiVariant1.svelte the polished editor shell
ui1/ editor sub-components (tool rail, inspector, …)
lib/
math/ Droste geometry, spiral, log map
render/ GL shaders, CPU worker
stores/ image + selection state
persistence.ts session restore
ui1/ editor state machine, exports
public/
Droste_1260359-nevit.jpg default sample image (CC BY-SA 3.0, Nevit Dilmen)
```sh
npm install # or: bun install
npm run dev # or: bun run dev → open the URL Vite prints
npm run build # → dist/
npm run preview # serve dist/ locally
```

---

## Credits

- **The Print Gallery** — M. C. Escher, 1956. The lithograph that gave this its name and
its blind spot.
- **The mathematical completion** — Bart de Smit and Hendrik Lenstra, 2003.
*Artful Mathematics: The Heritage of M. C. Escher.* Notices of the AMS.
- **Default image** — *Droste effect* photograph by [Nevit Dilmen](https://commons.wikimedia.org/wiki/File:Droste_1260359-nevit.jpg),
used under [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/).

---
Svelte 5 (runes) + Vite. Rendering is WebGL via [twgl.js](https://twgljs.org/) with a CPU-worker fallback; exports use `<canvas>.toBlob` (PNG), `MediaRecorder` (MP4/WebM), and [`gifenc`](https://github.com/mattdesl/gifenc) (GIF). No backend by design.

## Licence
</details>

The code is released under the GNU Affero General Public License v3.0 or later; see
`LICENSE`.
## Credits & licence

The default sample image is not relicensed as AGPL. It carries its own CC BY-SA 3.0
licence; see `public/ATTRIBUTION.txt`.
- **Print Gallery** / *Prentententoonstelling* — M. C. Escher, 1956. Escher's works are © The M. C. Escher Company; referenced here by description and link, not reproduced.
- **The mathematical completion** — Bart de Smit & Hendrik Lenstra, *The Mathematical Structure of Escher's Print Gallery*, [Notices of the AMS, 2003](https://www.ams.org/notices/200304/fea-escher.pdf).
- **Demo image** — *Droste effect* by [Nevit Dilmen](https://commons.wikimedia.org/wiki/File:Droste_1260359-nevit.jpg), [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) (see `public/ATTRIBUTION.txt`).
- **Code** — [AGPLv3 or later](LICENSE) (the sample image keeps its own CC BY-SA 3.0 licence).
Binary file added docs/screenshots/dictionary-card-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/dictionary-card-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/explorable-polar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/explorable.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading