Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cfbc4dd
Add 3D Pipes wallpaper
vaprdev Oct 12, 2026
4fbdafe
Tune 3D Pipes and add it to the README and GIF list
vaprdev Oct 12, 2026
b581fea
Add the Mystify wallpaper
vaprdev Oct 12, 2026
c15f4d5
Add Flying OpenCode wallpaper
vaprdev Oct 12, 2026
b78b1ac
Mystify: no hurry on a finished task, shorter-lived copies
vaprdev Oct 12, 2026
561f66a
Add the 3D Maze wallpaper
vaprdev Oct 12, 2026
8714ad0
Tune Flying OpenCode and add it to the README and GIFs
vaprdev Oct 12, 2026
a7bdb1b
Maze: wandering walk, visible polyhedra, cheaper shading, long check …
vaprdev Oct 12, 2026
665c0d3
Document the 3D Maze wallpaper; quicker turns
vaprdev Oct 12, 2026
21ce9dc
Maze: longer looks, darker sunset polyhedra
vaprdev Oct 12, 2026
de11eae
Merge branch 'pipes' into collections
vaprdev Oct 12, 2026
179e38f
Merge branch 'mystify' into collections
vaprdev Oct 12, 2026
2f0f503
Merge branch 'flying-opencode' into collections
vaprdev Oct 12, 2026
8baec2c
Merge branch 'maze' into collections
vaprdev Oct 12, 2026
1c68265
Group wallpapers into collections; give Akira and Classics one fixed …
vaprdev Oct 12, 2026
34d13b9
Arrange the README gallery and docs by collection
vaprdev Oct 12, 2026
3c9f56c
Mystify: compact figures that glide every frame, with a continuous ec…
vaprdev Oct 12, 2026
3936556
Keep time of day, season and weather to Landscapes
vaprdev Oct 12, 2026
9752065
Merge branch 'landscape-only' into collections
vaprdev Oct 12, 2026
72ff099
Mystify: full-screen polygons bouncing off the edges, gliding with st…
vaprdev Oct 12, 2026
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
18 changes: 16 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const meadow: Wallpaper = {
id: "meadow", // what /wallpaper takes: lowercase letters, digits and dashes
name: "Meadow",
description: "Rolling hills with a tree under drifting clouds",
collection: "Landscapes", // the menu's group; see Collections and fixed looks
activity: { calm: "…", lively: "…", teeming: "…" }, // what each level shows, for the menu
scrim: { day: [top, middle, bottom], sunset: […], night: […] }, // see Scrim
create: (settings) => new Meadow(settings), // activity and time, and optionally season and weather
Expand Down Expand Up @@ -240,6 +241,17 @@ single `node()`s into its coarse grid, then `apply()` it once after the things i
areas and dims itself behind the middle of the screen; `firelight()` there gives a slow flicker. Keep the area behind
text no brighter or busier than before, and keep flicker slow.

## Collections and fixed looks

`collection` puts a wallpaper in one of the menu's groups: `Landscapes`, `Akira`, `Classics`, or a name of your own
for a new group. The menu's Collection row picks one, and shuffle stays within it. Without one, a wallpaper joins
Landscapes, or Classics if it has a fixed look.

A wallpaper that only makes sense at one time of day, such as an old screensaver, sets `fixedTime: "night"` (or `day`
or `sunset`). It then always gets that time with no season or weather, whatever the settings, the menu hides its time,
season and weather rows, auto's crossfades pass it by, and the README and dev tools show it at that one time (`screenshots/<id>.png`).
`scrim` still needs all three times, though only the fixed one is used.

## Seasons and weather

`settings.season` is spring, summer, autumn or winter, or missing for the classic look. Change what the season would
Expand Down Expand Up @@ -355,6 +367,8 @@ bun dev/gif.ts all # re-renders the README's GIFs
- `check.ts` runs the real engine on a mock 160x45 screen. "cells change per frame" is flicker: lower is calmer.
`--quiet` prints only failures, and it fails on flicker above `--max-flicker` (150) and, if given, on frames slower
than `--max-ms`; CI runs `--quiet --max-ms=60`. Dev renders seed `Math.random`, so they are reproducible.
Wallpapers whose whole view moves now and then (the maze's walking camera, listed in `LONG`) play for two minutes,
so their flicker is averaged over moving and still stretches alike.
Errors thrown in `step` or `render` show up here; inside OpenCode they are only logged to `WALLPAPER_DEBUG`.
`--verify-dirty` plays each combination twice, refitting every cell (`WALLPAPER_DIRTY=0`) and then only what
changed, with text edits, redraws, agent events and a crossfade mixed in, and fails on any cell that differs.
Expand All @@ -379,8 +393,8 @@ bun dev/gif.ts all # re-renders the README's GIFs
1. Put it in `wallpapers/<id>.ts` as a named export, and add it to `wallpapers/index.ts`.
2. Run `bun run typecheck` and `bun dev/check.ts <id>`, and look at `dev/snap.ts` and `dev/preview.ts` output at every
time of day.
3. Add it to the README's wallpaper list, gallery and GIF grid. Add its best-looking time of day to `BEST` in
`dev/gif.ts`. CI publishes its screenshots once it's merged; a maintainer adds its GIF (see below).
3. Add it to its collection's line in the README's wallpaper list and to that collection's gallery. Unless it has a
fixed time, add its best-looking time of day to `BEST` in `dev/gif.ts`. CI publishes its screenshots once it's merged; a maintainer adds its GIF (see below).

## README images

Expand Down
40 changes: 28 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ Type `/wallpaper` to open the menu and pick a wallpaper; none shows until you do

![The wallpaper menu](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/menu.png)

- **Collection:** Landscapes, Akira, Classics or all: which wallpapers the menu and shuffle pick from.
- **Activity:** calm, lively or teeming: how much moves behind your work.
- **Time:** day, sunset or night, or auto to follow your clock.
- **Season and weather:** follow the calendar, or the real weather where you are (`/wallpaper location Berlin`).
- **Time, season and weather:** for Landscapes. Time follows your clock or stays fixed, seasons follow the calendar,
and weather can be the real weather where you are (`/wallpaper location Berlin`). Akira and Classics keep one look.
- **Brightness, shuffle, per project and power:** subtle to vivid, a new wallpaper every hour, one per project, and
slowing down when idle.

Expand All @@ -34,8 +35,9 @@ an error. With mouse support, clicking empty background nudges the scene, and ea

## Wallpapers

`ocean` · `desert` · `jungle` · `space` · `farm` · `tundra` · `beach` · `city` · `zen` · `prehistoric` · `rider` ·
`megacity` · `dome` · `haunted-house` · `flowerbox`
- **Landscapes:** `ocean` · `desert` · `jungle` · `space` · `farm` · `tundra` · `beach` · `city` · `zen` · `prehistoric` · `haunted-house`
- **Akira:** `rider` · `megacity` · `dome`
- **Classics:** `flowerbox` · `maze` · `pipes` · `mystify` · `flying-opencode`

| Spring | Summer | Autumn | Winter |
| --- | --- | --- | --- |
Expand All @@ -61,7 +63,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).

## Gallery

At teeming, played back at 3x speed:
At teeming; the GIFs play at 3x speed.

### Landscapes

| | |
| --- | --- |
Expand All @@ -70,9 +74,7 @@ At teeming, played back at 3x speed:
| ![Farm in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/farm.gif) | ![Tundra in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/tundra.gif) |
| ![Beach in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/beach.gif) | ![City in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/city.gif) |
| ![Zen garden in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/zen.gif) | ![Prehistoric in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/prehistoric.gif) |
| ![Rider in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/rider.gif) | ![Megacity in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/megacity.gif) |
| ![Dome in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/dome.gif) | ![Haunted house in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/haunted-house.gif) |
| ![Flower Box in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flowerbox.gif) | |
| ![Haunted house in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/haunted-house.gif) | |

| | Day | Sunset | Night |
| --- | --- | --- | --- |
Expand All @@ -86,11 +88,25 @@ At teeming, played back at 3x speed:
| City | ![City by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/city.png) | ![City at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/city-sunset.png) | ![City at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/city-night.png) |
| Zen garden | ![Zen garden by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/zen.png) | ![Zen garden at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/zen-sunset.png) | ![Zen garden at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/zen-night.png) |
| Prehistoric | ![Prehistoric by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/prehistoric.png) | ![Prehistoric at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/prehistoric-sunset.png) | ![Prehistoric at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/prehistoric-night.png) |
| Rider | ![Rider by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/rider.png) | ![Rider at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/rider-sunset.png) | ![Rider at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/rider-night.png) |
| Megacity | ![Megacity by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/megacity.png) | ![Megacity at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/megacity-sunset.png) | ![Megacity at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/megacity-night.png) |
| Dome | ![Dome by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/dome.png) | ![Dome at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/dome-sunset.png) | ![Dome at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/dome-night.png) |
| Haunted house | ![Haunted house by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/haunted-house.png) | ![Haunted house at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/haunted-house-sunset.png) | ![Haunted house at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/haunted-house-night.png) |
| Flower Box | ![Flower Box by day](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flowerbox.png) | ![Flower Box at sunset](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flowerbox-sunset.png) | ![Flower Box at night](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flowerbox-night.png) |

### Akira

| | Still | In motion |
| --- | --- | --- |
| Rider | ![Rider](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/rider.png) | ![Rider in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/rider.gif) |
| Megacity | ![Megacity](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/megacity.png) | ![Megacity in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/megacity.gif) |
| Dome | ![Dome](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/dome.png) | ![Dome in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/dome.gif) |

### Classics

| | Still | In motion |
| --- | --- | --- |
| Flower Box | ![Flower Box](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flowerbox.png) | ![Flower Box in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flowerbox.gif) |
| 3D Maze | ![3D Maze](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/maze.png) | ![3D Maze in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/maze.gif) |
| 3D Pipes | ![3D Pipes](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/pipes.png) | ![3D Pipes in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/pipes.gif) |
| Mystify | ![Mystify](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/mystify.png) | ![Mystify in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/mystify.gif) |
| Flying OpenCode | ![Flying OpenCode](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flying-opencode.png) | ![Flying OpenCode in motion](https://raw.githubusercontent.com/vaprdev/opencode-wallpapers/assets/flying-opencode.gif) |

## License

Expand Down
22 changes: 15 additions & 7 deletions dev/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,25 @@ const cursor = { x: 10, y: 40, visible: true }
let failed = 0
let slowest = { ms: -1, name: "" }
let busiest = { flicker: -1, name: "" }
const frames = 240
const runs = (ids.length ? wallpapers.filter((w) => ids.includes(w.id)) : wallpapers).flatMap((w) => ACTIVITIES.flatMap((activity) => TIMES.map((time) => ({ wallpaper: w, activity, time }))))
// Wallpapers whose whole view moves now and then, such as the maze's walking camera, change hundreds of cells a frame
// while moving and almost none while still, so 16 seconds say more about where the run lands than about the scene.
// They play for two minutes instead, held to the same flicker limit; --verify-dirty, which keeps every frame of its
// shorter scripted run, checks only their cells.
const LONG = ["maze"]
const runs = (ids.length ? wallpapers.filter((w) => ids.includes(w.id)) : wallpapers).flatMap((w) => ACTIVITIES.flatMap((activity) => (w.fixedTime ? [w.fixedTime] : TIMES).map((time) => ({ wallpaper: w, activity, time }))))
for (const { wallpaper, activity, time } of runs) {
const reference = verify ? play(wallpaper, activity, time, "0").recording : undefined
const result = play(wallpaper, activity, time, "1", reference)
const long = LONG.includes(wallpaper.id)
const frames = long && !verify ? 1800 : 240
const reference = verify ? play(wallpaper, activity, time, "0", frames).recording : undefined
const result = play(wallpaper, activity, time, "1", frames, reference)
const errors = (await Bun.file(debug).exists()) ? (await Bun.file(debug).text()).split("\n").filter((l) => l.startsWith("error:")) : []
rmSync(debug, { force: true })
const name = `${wallpaper.id} ${activity} ${time}`
if (result.ms > slowest.ms) slowest = { ms: result.ms, name }
if (result.flicker > busiest.flicker) busiest = { flicker: result.flicker, name }
const problems = [
errors.length && `${errors.length} errors`,
result.flicker > maxFlicker && `flicker over ${maxFlicker}`,
result.flicker > maxFlicker && !(long && verify) && `flicker over ${maxFlicker}`,
result.ms > maxMs && `slower than ${maxMs} ms`,
result.mismatches && `${result.mismatches} cells differ from a full refit, first ${result.first}`,
result.uncovered && `${result.uncovered} pixels drawn outside the touched boxes, first ${result.where}`,
Expand All @@ -59,7 +65,7 @@ if (failed) process.exit(1)

// Plays one combination, with WALLPAPER_DIRTY set to dirty. Under --verify-dirty it records every frame, and compares
// each against the reference recording when given.
function play(wallpaper: Wallpaper, activity: Activity, time: Time, dirty: string, reference?: Uint16Array[]) {
function play(wallpaper: Wallpaper, activity: Activity, time: Time, dirty: string, frames: number, reference?: Uint16Array[]) {
process.env.WALLPAPER_DIRTY = dirty
// A fresh engine and the same random stream per combination, so a run doesn't depend on which others ran before it.
const screen = mockScreen()
Expand Down Expand Up @@ -123,7 +129,9 @@ function script(f: number, engine: Engine, wallpaper: Wallpaper, activity: Activ
if (f === 150) engine.react("error")
if (f === 200) engine.react("idle")
if (f === 180) engine.start(wallpaper, { activity, time, season, weather, brightness: "vivid" })
if (f === 205) engine.start(wallpaper, { activity, time: time === "night" ? "day" : "night", season, weather, brightness: "vivid" })
if (f === 205 && !wallpaper.fixedTime) engine.start(wallpaper, { activity, time: time === "night" ? "day" : "night", season, weather, brightness: "vivid" })
// A wallpaper with a fixed time ignores a new time, so it crossfades to another activity instead.
if (f === 205 && wallpaper.fixedTime) engine.start(wallpaper, { activity: activity === "calm" ? "teeming" : "calm", time, season, weather, brightness: "vivid" })
}

// A wallpaper whose scenes check, after every render, that each pixel that differs from the cached background lies in
Expand Down
14 changes: 9 additions & 5 deletions dev/gif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const SPEED = 3
const FPS = 12.5
const START = 45
const FADE = 1.5
// The time of day each wallpaper looks best at in the README; others use day.
const BEST: Record<string, Time> = { ocean: "day", desert: "sunset", jungle: "day", space: "sunset", farm: "sunset", tundra: "night", beach: "sunset", city: "night", zen: "sunset", prehistoric: "sunset", rider: "day", megacity: "night", dome: "night", "haunted-house": "night", flowerbox: "night" }
// The time of day each wallpaper with times of day looks best at in the README; others use day.
const BEST: Record<string, Time> = { ocean: "day", desert: "sunset", jungle: "day", space: "sunset", farm: "sunset", tundra: "night", beach: "sunset", city: "night", zen: "sunset", prehistoric: "sunset", "haunted-house": "night" }
// Wallpapers in the hero GIF at the top of the README, each shown for HERO_SECONDS.
const HERO = ["beach", "tundra", "jungle", "desert", "space"]
const HERO_SECONDS = 2
Expand All @@ -31,22 +31,26 @@ mkdirSync("dev/out", { recursive: true })
if (args[0] === "all") {
assets()
for (const wallpaper of WALLPAPERS) {
const settings = { activity: "teeming" as const, time: BEST[wallpaper.id] ?? "day" }
const settings = { activity: "teeming" as const, time: best(wallpaper) }
await write(`screenshots/${wallpaper.id}.gif`, [{ wallpaper, settings, seconds: 8 }], 480, 268, FADE)
}
const hero = HERO.flatMap((id) => WALLPAPERS.filter((w) => w.id === id))
await write("screenshots/hero.gif", hero.map((wallpaper) => ({ wallpaper, settings: { activity: "teeming", time: BEST[wallpaper.id] ?? "day" }, seconds: HERO_SECONDS })), 720, 400, 0)
await write("screenshots/hero.gif", hero.map((wallpaper) => ({ wallpaper, settings: { activity: "teeming", time: best(wallpaper) }, seconds: HERO_SECONDS })), 720, 400, 0)
}
if (args[0] !== "all") {
const wallpaper = wallpapers.find((w) => w.id === args[0])
if (!wallpaper) throw new Error(`usage: bun dev/gif.ts <all|${wallpapers.map((w) => w.id).join("|")}> [activity] [time] [WxH] [seconds]`)
const activity = ACTIVITIES.find((a) => args.includes(a)) ?? "teeming"
const time = TIMES.find((t) => args.includes(t)) ?? BEST[wallpaper.id] ?? "day"
const time = wallpaper.fixedTime ?? TIMES.find((t) => args.includes(t)) ?? best(wallpaper)
const size = (args.find((arg) => /^\d+x\d+$/.test(arg)) ?? "480x268").split("x").map(Number)
const seconds = Number(args.find((arg) => /^[\d.]+$/.test(arg)) ?? 8)
await write(`dev/out/${wallpaper.id}-${activity}-${time}.gif`, [{ wallpaper, settings: { activity, time }, seconds }], size[0], size[1], FADE)
}

function best(wallpaper: Wallpaper) {
return wallpaper.fixedTime ?? BEST[wallpaper.id] ?? "day"
}

async function write(path: string, shots: Shot[], W: number, H: number, fadeSeconds: number) {
const start = Bun.nanoseconds()
const fade = Math.round(fadeSeconds * FPS)
Expand Down
2 changes: 1 addition & 1 deletion dev/poke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { wallpapers } from "./wallpapers"

const [id, ...rest] = process.argv.slice(2)
const activity = ACTIVITIES.find((a) => rest.includes(a)) ?? "calm"
const time = TIMES.find((t) => rest.includes(t)) ?? "day"
const [fx, fy, ...after] = rest.filter((arg) => /^[\d.]+$/.test(arg)).map(Number)
const wallpaper = wallpapers.find((w) => w.id === id)
if (!wallpaper || fy === undefined) throw new Error(`usage: bun dev/poke.ts <${wallpapers.map((w) => w.id).join("|")}> [activity] [time] <fx> <fy> [seconds...]`)
const time = wallpaper.fixedTime ?? TIMES.find((t) => rest.includes(t)) ?? "day"
mkdirSync("dev/out", { recursive: true })
const scene = wallpaper.create({ activity, time })
scene.resize(720, 400)
Expand Down
2 changes: 1 addition & 1 deletion dev/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { wallpapers } from "./wallpapers"

const [id, ...rest] = process.argv.slice(2)
const activity = ACTIVITIES.find((a) => rest.includes(a)) ?? "calm"
const time = TIMES.find((t) => rest.includes(t)) ?? "day"
const season = SEASONS.find((s) => rest.includes(s))
const weather = WEATHERS.find((w) => rest.includes(w))
const layout = (["home", "session"] as const).find((l) => rest.includes(l))
Expand All @@ -19,6 +18,7 @@ const [w, h] = size ? size.split("x").map(Number) : [W, H]
const seconds = rest.find((arg) => /^[\d.]+$/.test(arg)) ?? "20"
const wallpaper = wallpapers.find((w) => w.id === id)
if (!wallpaper) throw new Error(`usage: bun dev/preview.ts <${wallpapers.map((w) => w.id).join("|")}> [seconds]`)
const time = wallpaper.fixedTime ?? TIMES.find((t) => rest.includes(t)) ?? "day"
process.env.WALLPAPER_OCTANTS ??= "1"
mkdirSync("dev/out", { recursive: true })
const screen = mockScreen({ W: w, H: h })
Expand Down
Loading
Loading