Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
6f14895
Give the ocean night lighter water, dark rimmed silhouettes and plank…
vaprdev Oct 10, 2026
34b3597
Light the prehistoric night: brighter horizon, moonlit rims and lava …
vaprdev Oct 10, 2026
56e5bbc
Light the desert night: brighter horizon and dunes, moonlit rims and …
vaprdev Oct 10, 2026
620c250
Light the jungle night: brighter mist band, stronger moon rims and fi…
vaprdev Oct 10, 2026
f8b2138
Give farm animals and the farmer jointed legs, real walk cycles and t…
vaprdev Oct 10, 2026
a091d6a
Light the space night: airglow, planet-lit rims on rocks and craft, a…
vaprdev Oct 10, 2026
ea6d158
Hoist the moon color out of its loop
vaprdev Oct 10, 2026
8935602
Light the farm night: brighter horizon and fields, moonlit rims, an o…
vaprdev Oct 10, 2026
bd40492
Walk the dinosaurs on jointed legs with swinging tails
vaprdev Oct 10, 2026
0f1945a
Light the tundra night: brighter horizon, stronger moon rims and iglo…
vaprdev Oct 10, 2026
daff6a7
Walk the tundra animals on jointed legs and bring the fox down to size
vaprdev Oct 10, 2026
daba5ef
Light the beach night: brighter horizon, a bonfire and stronger foam …
vaprdev Oct 10, 2026
af93597
Give each wallpaper its own sunset
vaprdev Oct 10, 2026
a4b0fd4
Shape the desert coyote and let its head and ears settle after a howl
vaprdev Oct 10, 2026
7e9e687
Light the city night: brighter skyglow behind the skyline, stronger r…
vaprdev Oct 10, 2026
2572179
Shape the jungle jaguar and let the monkey's limbs trail its swing
vaprdev Oct 10, 2026
8a5629c
Light the zen night: brighter horizon and mist, lantern light on the …
vaprdev Oct 10, 2026
99698a1
Shape the beach dolphins and crab, and walk the crab on jointed legs
vaprdev Oct 10, 2026
152f444
Give the zen heron jointed, wading legs and trailing wingtips and cre…
vaprdev Oct 10, 2026
5d45e9f
Walk city pedestrians with bending knees, planted steps and swinging …
vaprdev Oct 10, 2026
35cc190
Bring the ocean's reef fish down to scale and let the turtle's flippe…
vaprdev Oct 10, 2026
1a1b951
Ground creatures and props with contact and cast shadows
vaprdev Oct 10, 2026
c75c1ff
Reflect the sky and scenery in the beach sea, prehistoric lake and tu…
vaprdev Oct 10, 2026
71a389c
Expose a coarse map of where text sits to scenes, and add home and se…
vaprdev Oct 10, 2026
7ad7bf3
Move focal points and resting places to the sides, top and bottom, cl…
vaprdev Oct 10, 2026
89955cc
Shade clouds from the sun or moon and give each wallpaper its own clo…
vaprdev Oct 10, 2026
894c1b5
Add a shared wind with gusts that roll across every outdoor scene
vaprdev Oct 10, 2026
96220f1
Richer daytime color, aerial haze and framing for every wallpaper
vaprdev Oct 10, 2026
48f23a2
Draw bolder detail on terminal cells and steady quadrant cells
vaprdev Oct 10, 2026
436ccb6
Make thin clouds read in the terminal and document the cloud helpers
vaprdev Oct 10, 2026
336f902
Slow the beach's crest highlights to keep flicker down
vaprdev Oct 10, 2026
7f86034
Keep the octopus, ice fisher and penguins clear of the prompt
vaprdev Oct 10, 2026
549561d
Keep the beach's night glints saturated
vaprdev Oct 10, 2026
aa39a79
Soften tundra drift ripples by day so the blue shadows read as broad …
vaprdev Oct 10, 2026
5f30f1d
Keep fronds and corn near-silhouettes under rim light on terminal cells
vaprdev Oct 10, 2026
ebef9cd
Keep the ocean's ridge tones from brightening the day
vaprdev Oct 10, 2026
63b84f5
Document the daytime grading helpers
vaprdev Oct 10, 2026
5e0c8cd
Export the night light helpers and document night light structure
vaprdev Oct 10, 2026
9cd02d8
Note the water reflection helper in CONTRIBUTING
vaprdev Oct 10, 2026
4ab5563
Document shadows and show them in the template
vaprdev Oct 10, 2026
5fd7998
Document the creature helpers and export them from the API
vaprdev Oct 10, 2026
7a7c5f0
Drop the unused shade tint override
vaprdev Oct 10, 2026
f49e62d
Light up ocean motes in the shafts, steady the gust flutter, and docu…
vaprdev Oct 10, 2026
2a1ac6c
Document drawing for terminal cells and export blade
vaprdev Oct 10, 2026
38d0c6e
Let prehistoric fronds catch a little more rim light on cells
vaprdev Oct 10, 2026
15aedb0
Document composition and the text map
vaprdev Oct 10, 2026
f26a825
Merge sunsets
vaprdev Oct 10, 2026
f308d7e
Merge day-color
vaprdev Oct 10, 2026
29ff4fe
Merge night-contrast
vaprdev Oct 10, 2026
a4a9fd4
Merge clouds
vaprdev Oct 10, 2026
3cceaa4
Merge water
vaprdev Oct 10, 2026
a553b85
Merge shadows
vaprdev Oct 10, 2026
e1f8321
Merge cell-grid
vaprdev Oct 10, 2026
fefad88
Merge creatures
vaprdev Oct 10, 2026
517f3b8
Merge wind
vaprdev Oct 10, 2026
48ce798
Merge composition
vaprdev Oct 10, 2026
62d08e2
Fix shadow stacking, sunset clouds, palm reflections, gust lean, crea…
vaprdev Oct 10, 2026
1a79bfc
Regenerate screenshots, GIFs and the menu, and document the new helpers
vaprdev Oct 10, 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
90 changes: 79 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@ when idle at 5 fps, never more than 0.1 s at a time), then `render()`, and reads
`src/canvas.ts`) and you only write `step` and `render`. Brightness is applied by the engine, so scenes ignore it.

The grid is small: a 160x45 terminal is 320x180 sub-pixels in character mode (2x4 per cell), and 480x270 in pixel mode.
`bun dev/snap.ts` renders at 720x400. Anything thinner than about 2 sub-pixels flickers or vanishes, so size things as
fractions of the height `H`. Most scenes use x from 0 to `A` (the aspect ratio, `W / H`) and y from 0 to 1, and multiply
by `H` to get pixels.
`bun dev/snap.ts` renders at 720x400. Size things as fractions of the height `H`. Most scenes use x from 0 to `A` (the
aspect ratio, `W / H`) and y from 0 to 1, and multiply by `H` to get pixels.

In character mode each cell shows only two colors, so anything thinner than about 2 sub-pixels, and any texture finer
than a cell, turns into speckle and shimmers as it moves. `Canvas` sets `this.cells` then (not in snaps or pixel mode),
so a scene can draw bolder there: `thick(radius, min)` gives a stroke radius in pixels that never drops below `min` on
cells, `blade(spine, widths, color)` draws a frond's leaflets as one scalloped blade, and textures can coarsen or drop
out. Judge it with `bun dev/preview.ts`, and with `WALLPAPER_OCTANTS=0` for quadrant terminals.

### step and render

Expand Down Expand Up @@ -97,7 +102,9 @@ This is the biggest single saving: a full-screen gradient with noise costs more
| `polygon(points, color, alpha?)` | An anti-aliased polygon in a flat color, for straight-edged things like buildings and boats |
| `shape(parts, lighting, light?)` | Rounded parts joined into one lit form, for creatures, people and plants |

For per-pixel effects such as water, sand or fog, loop over `hdr` directly.
For per-pixel effects such as water, sand or fog, loop over `hdr` directly. `reflect()` in `src/water.ts` mirrors
other rows of the scene into water rows with drifting ripples, a tint and a per-pixel weight; call it each frame over
only the water's rows (as the beach and the prehistoric lake do), or once in `layout()` for still water such as ice.

### Shapes

Expand Down Expand Up @@ -132,12 +139,42 @@ details), and values in between soften it.
of height), `core` and `glow` colors, `near` and `wide` glow strengths, and `moon: true` for a cratered moon.
- `makeStars(count, top)` and `paintStars(hdr, W, H, stars, time, top, brightness)` draw slowly twinkling stars that
fade toward `top`. Stars are tinted from `STAR_TINTS`, never white.
- `makeClouds(count, puffy, y0, y1)`, `driftClouds(clouds, A, dt)` and `paintClouds(hdr, W, H, clouds, top, bottom,
alpha, puffy)` give puffy cumulus or thin streaks, shaded from `top` to `bottom`.
- `makeClouds(count, kind, y0, y1)`, `driftClouds(clouds, A, dt)` and `paintClouds(hdr, W, H, clouds, top, bottom,
alpha, orb)` give clouds of one kind: `cumulus`, `towering` (standing on y, for a horizon), `cirrus`, `wisp`,
`stratus` or `ash`. They are lit from the orb: the brighter of `top` and `bottom` faces it, and clouds near it are
backlit with glowing edges. Pass no orb under an overcast deck to light them evenly from above. Each cloud's shape
is built once per screen size, so drawing them each frame is cheap. Heaped kinds read best by day and at night;
thin streaks (`cirrus`, `stratus`) catch a low sunset sun better.
- `makeShadows(count, y0, y1)` and `paintShadows(hdr, W, H, shadows, ground, orb)` drift soft cloud shadows across
the ground below `ground` under a high sun; `paintHaze(hdr, W, H, y0, y1, color, alpha)` paints a static haze band.

`src/grade.ts` gives daytime depth in `layout()`: `haze(hdr, sky, W, H, y0, y1, far, near?, top?)` fades what was
painted toward a copy of the sky taken right after `paintSky` (call it after each layer so farther ones fade more), and
`mottle(hdr, W, H, horizon, bottom, size, warm, cool)` lays broad warm and cool patches over the ground in perspective.
Setting `this.frame` (0 to 1) in the constructor darkens the bottom corners to frame a bright foreground.

`src/math.ts` has `TAU`, `lerp`, `clamp`, `smoothstep`, `rand`, stable hashes (`hash`, `hash2`, `hashString`), smooth
noise (`noise1`, `noise2`, `fbm1`, `fbm2`), `hsv` and the `RGB` type.

### Shadows

`src/shadow.ts` grounds things standing on the ground. Make one `Shade` with `sunShade(time, look.orb,
weather.covered)`, then call `groundShadow(hdr, W, H, shade, x, y, w, h, tip?, lift?)` with the ground point under a
thing, its width and height in pixels, before drawing it: static things in `layout()`, moving ones each frame. It draws a
soft contact shadow and a cast shadow away from the light: short by day, long and spreading from the low sun at sunset,
faint at night, contact only under cloud. `lift` (pixels off the ground) fades the contact shadow and slides the cast one
away, for hops and lifts. Shadows multiply the ground toward a sky-tinted color, so they never gray it, and where
they overlap the ground darkens only as much as the deepest of them, not stacking into dark patches.

### Creatures

`src/creature.ts` animates walkers. Pose a creature in body coordinates with `body(x, y, S, turn)` (u forward, v
down, w across), and keep a `Gait` per walker: `stepGait(g, distance / stride, dt, face)` advances the step cycle with
the distance walked, so planted feet don't slide, eases between standing and walking, turns through a three-quarter
view instead of flipping, and rings a springy `lag` for tails and ears to trail. `quadruped()` draws four jointed
legs in diagonal pairs, `limb()` any two-segment limb with a bending joint, `stride()` and `bob()` one foot's step and
the body's rise, and `chain()` a tapered tail or neck along a curve.

## Time of day

Each wallpaper keeps everything that changes with the time of day in one `LOOKS: Record<Time, Look>`: sky stops, the
Expand All @@ -158,6 +195,12 @@ sunset scenes by about a fifth and night scenes barely, so night keeps its color
Night must not use white or gray highlights, because OpenCode's text is white. Use saturated, tinted colors instead:
cobalt and teal moonlight, a gold moon, amber lamps and windows, tinted stars, glowing plankton or fireflies.

Give night clear light structure so things stay readable: a lighter band at the horizon (or, underwater, toward the
surface) for silhouettes to stand against, a strong rim `light` from the moon's side, and a few local pools of warm or
glowing light. `lightPool()` in `src/light.ts` lights an ellipse in the light's own color, so ground and creatures
passing through it pick it up; draw it each frame after the creatures it should light. `campfire()` and
`fireflyLight()` there are ready-made pools. Keep the area behind text no brighter or busier than before.

## Seasons and weather

`settings.season` is spring, summer, autumn or winter, or missing for the classic look. Change what the season would
Expand All @@ -169,13 +212,36 @@ make one `WeatherLayer` (from `src/weather.ts`) with the weather, time of day an
Skip stars when `covered` is set, and color clouds with `scud` when it is. Passing `light` as the last argument gives a
seasonal shower or flurry without the cloud deck, as the farm does in winter. `wallpapers/farm.ts` shows all of it.

## Wind

Every outdoor scene shares one wind from `src/wind.ts`, so a gust rolls across the whole screen from left to right
every minute or so and then dies away. `gust(x, t)` is how hard it blows at x (screen heights) and scene time t, from 0
to 1: add it to anything that drifts, such as petals, snow or dust. `sway(x, t, phase, rate)` replaces a plant's own
`Math.sin(t * rate + phase)`: the same rocking in calm air, plus a lean downwind as a gust passes. For plants painted
once in `layout()`, a `SwayLayer` bends them without redrawing: call `begin()` before painting them, `end()` after
with how far their tips move and a weight from roots to tips, and `draw()` right after copying the background.

## Scrim

Text gets a soft scrim that fades the scene toward a dark color around it. `scrim` gives that color (0 to 255) for
each time of day at the top, middle and bottom of the screen. Darker, more saturated versions of the scene's own colors
at those depths read better than black: dark sky blue at the top and dark green over grass by day, deep navy at night.
Check that text stays readable with `bun dev/preview.ts` at every time of day.

## Composition

OpenCode's text sits in the middle of the screen: the home screen has a centered logo and a 76-column prompt across
the middle third of the height, and a session has a column of messages, a prompt at the bottom and, from 120 columns,
a sidebar on the right. Put the things people should see (a barn, a volcano, a waterfall) toward the left and right
edges, in the top band or along the bottom, measured in screen heights from the nearest edge (`A - 0.3`, not `0.8 * A`)
so they stay clear on wide and narrow terminals alike. Keep the middle band to calm scenery such as sky, water or open
field. `bun dev/preview.ts <id> home 100x30` shows a real layout at any size.

The engine also tells scenes where text actually is, about once a second. `this.openSpot(x0, x1, y0, y1)` picks a
random point in a box, the least covered of a few tries, for a creature's next stop or a visitor's spot, and
`this.openRow(y0, y1)` picks a height for something crossing the whole screen. Both are plain random picks until the
engine has sent its map, and in `dev/snap.ts`.

## Activity

- **calm** is scenery with something rare now and then: water, wind, clouds, one creature passing every minute or so.
Expand Down Expand Up @@ -232,7 +298,7 @@ a lot with machine load, so compare against an existing wallpaper measured back
bun run typecheck
bun dev/check.ts [id...] [season] [weather] # every activity and time: errors, ms/frame, flicker
bun dev/snap.ts <id> [activity] [time] [season] [weather] [seconds...] # scene PNGs in dev/out/, and ms per render
bun dev/preview.ts <id> [activity] [time] [season] [weather] [seconds] # terminal cells behind text, as a PNG
bun dev/preview.ts <id> [activity] [time] [season] [weather] [home|session] [WxH] [seconds] # terminal cells behind text, as a PNG
bun dev/pixel-check.ts [id] [activity] [time] # real-pixel mode against a mock kitty terminal
bun dev/poke.ts <id> [activity] [time] <fx> <fy> [seconds...] # click at a point (fractions of the screen)
bun dev/fade.ts <id>:day <id>:night 50 # a frame halfway through a crossfade, and its cost
Expand All @@ -247,7 +313,8 @@ bun dev/gif.ts all # re-renders the README's GIFs
than `--max-ms`; CI runs `--quiet --max-ms=60`. Dev renders seed `Math.random`, so they are reproducible.
Errors thrown in `step` or `render` show up here; inside OpenCode they are only logged to `WALLPAPER_DEBUG`.
- `snap.ts` renders the scene directly, before terminal conversion, for judging the art.
- `preview.ts` shows what the terminal will show, scrim included.
- `preview.ts` shows what the terminal will show, scrim included. `home` and `session` lay the text out like OpenCode's
home screen or a busy session, and a size such as `240x60` or `100x30` replaces 160x45.
- `dev/pty.ts [seconds] [keys...]` runs a real OpenCode in a pseudo-terminal and types each key argument, pausing
1.5 s after each; `\r` is Enter and `\e` is Escape. `dev/pty-kitty.ts [id] [seconds]` does the same as a kitty
terminal in pixel mode. Run them with a throwaway profile (fresh `XDG_DATA_HOME`, `XDG_CONFIG_HOME`,
Expand All @@ -272,9 +339,10 @@ bun dev/gif.ts all # re-renders the README's GIFs

## Your own package

A wallpaper package imports the API from `opencode-wallpapers/api`, which has `Canvas`, `cap`, `ell`, the sky,
weather, flock, egg and math helpers, and the `Wallpaper` types, with no OpenCode or OpenTUI dependency. It isn't on
npm yet, so build a clone of this repository and depend on it by path:
A wallpaper package imports the API from `opencode-wallpapers/api`, which has `Canvas`, `cap`, `ell`, `blade`, the
sky, grade, shadow, light, water, creature, wind, weather, flock, egg and math helpers, and the `Wallpaper` types,
with no OpenCode or OpenTUI dependency. It isn't on npm yet, so build a clone of this repository and depend on it by
path:

```sh
(cd ~/code/opencode-wallpapers && bun install && bun run build)
Expand Down
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,27 @@ Every wallpaper has a day, sunset and night look. By default the time of day fol
sunset from 6pm (and at dawn, 6 to 7am), night from 8pm, fading from one to the next over 45 seconds. Pick one in the
menu to keep it fixed.

- **Ocean:** sunlit water by day, golden light at sunset, and at night dark moonlit water with glowing plankton
and, in teeming, a diver's torch.
- **Desert:** a high sun with sunlit rock and shadows by day, backlit silhouettes at sunset, and a moonlit night with
stars and the Milky Way, where an owl and bats take over the sky.
- **Ocean:** sunlit water by day, golden light at sunset, and at night moonlit blue water with dark silhouettes,
glowing plankton and, in teeming, a diver's torch.
- **Desert:** a high sun with sunlit rock and shadows by day, silhouettes and long shadows in a hot ochre dust haze at
sunset, and a moonlit night with stars, the Milky Way and a campfire, where an owl and bats take over the sky.
- **Jungle:** sunlit greens by day, warm backlit mist at sunset, and a moonlit night lit by fireflies, where the
jaguar's eyes glow.
- **Space:** the planet's sunlit side by day, an orbital sunrise with the star flaring at its edge at sunset, and its
night side with city lights and aurora under a bright Milky Way.
- **Farm:** sunny fields by day, a backlit barn at sunset, and a moonlit night with glowing windows and fireflies.
- **Farm:** sunny fields under drifting cloud shadows by day, golden-hour light and long shadows over warm grass at
sunset, and a moonlit night with glowing windows, an open barn door and fireflies.
- **Tundra:** crisp blue-white snow by day, pink alpenglow at sunset, and deep blue snow under a green and violet
aurora at night, with warm light from the igloos.
- **Beach:** turquoise water by day, a glittering path under the setting sun, and a moon path at night with glowing
bioluminescent waves.
- **City:** glass towers in the sun by day, backlit towers with the first lit windows at sunset, and at night windows,
lamps and neon in saturated colors mirrored in the river, with rain and a wet street in teeming.
- **Zen garden:** a sunlit pond mirroring the sky by day, the setting sun glittering in it at sunset, and at night
amber lanterns, fireflies and the gold moon reflected in dark blue water.
- **Prehistoric:** a hazy blue sky over glowing lava by day, backlit dinosaurs at sunset, and a moonlit night where
red and orange lava runs down the volcano, lights its smoke and shimmers on the lake.
- **Beach:** turquoise water mirroring the sky by day, a pastel pink and teal sunset over a pink-lit sea, and a moon
path at night with glowing bioluminescent waves and a bonfire on the sand.
- **City:** glass towers in the sun by day, a smoggy amber and magenta sunset dropping between the towers, and at
night windows, lamps and neon in saturated colors mirrored in the river, with rain and a wet street in teeming.
- **Zen garden:** a sunlit pond mirroring the sky by day, a soft peach and lavender sunset glittering in it, and at
night amber lanterns, fireflies and the gold moon reflected in dark blue water.
- **Prehistoric:** a hazy blue sky over glowing lava by day, dinosaurs under an ashy red sky with the sun dimmed by
smoke at sunset, and a moonlit night where red and orange lava runs down the volcano, lights its smoke and shimmers
on the lake.

## Seasons and weather

Expand Down Expand Up @@ -181,7 +183,9 @@ The plugin post-processes each frame OpenCode draws. Cells painted with OpenCode
by the scene, while colored backgrounds such as diffs and selections are left alone. Text keeps a soft scrim that fades
the scene toward a dark tint around it. Scenes render in HDR with bloom and tone mapping, then are fitted to two colors
per character cell. While one look fades into another, both scenes render and blend, scrim included, so only
transitions cost extra.
transitions cost extra. Scenes keep their landmarks toward the edges, and the engine tells them where text sits, so
creatures and visitors stop where they can be seen. Outdoor scenes share one wind: now and then a gust rolls across
the screen, bending plants and carrying petals, snow and dust.

## Make a wallpaper

Expand Down
2 changes: 1 addition & 1 deletion dev/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mkdirSync("dev/out", { recursive: true })
const debug = `${process.cwd()}/dev/out/check.log`
rmSync(debug, { force: true })
process.env.WALLPAPER_DEBUG = debug
process.env.WALLPAPER_OCTANTS = "1"
process.env.WALLPAPER_OCTANTS ??= "1"

const args = parseArgs({ allowPositionals: true, options: { quiet: { type: "boolean" }, "max-ms": { type: "string" }, "max-flicker": { type: "string", default: "150" } } })
const maxMs = Number(args.values["max-ms"] ?? Infinity)
Expand Down
Loading
Loading