diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c067710..a9ea2b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -176,6 +176,20 @@ each time of day at the top, middle and bottom of the screen. Darker, more satur 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 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. @@ -232,7 +246,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 [activity] [time] [season] [weather] [seconds...] # scene PNGs in dev/out/, and ms per render -bun dev/preview.ts [activity] [time] [season] [weather] [seconds] # terminal cells behind text, as a PNG +bun dev/preview.ts [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 [activity] [time] [seconds...] # click at a point (fractions of the screen) bun dev/fade.ts :day :night 50 # a frame halfway through a crossfade, and its cost @@ -247,7 +261,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`, diff --git a/dev/mock.ts b/dev/mock.ts index cf294bb..bd72d79 100644 --- a/dev/mock.ts +++ b/dev/mock.ts @@ -1,28 +1,30 @@ -// A mock 160x45 OpenCode screen driving the real engine, for headless checks. +// A mock OpenCode screen (160x45 unless sized) driving the real engine, for headless checks. import { createEngine } from "../src/engine" export const W = 160 export const H = 45 export const FPS = 15 -export function mockScreen(options: { kitty?: boolean } = {}) { +export function mockScreen(options: { kitty?: boolean; W?: number; H?: number } = {}) { + const w = options.W ?? W + const h = options.H ?? H // The engine advances on wall-clock time; each performance.now() call moves one engine frame forward. let clock = 0 performance.now = () => (clock += 1000 / FPS) - const buffers = { char: new Uint32Array(W * H), fg: new Uint16Array(W * H * 4), bg: new Uint16Array(W * H * 4), attributes: new Uint32Array(W * H) } + const buffers = { char: new Uint32Array(w * h), fg: new Uint16Array(w * h * 4), bg: new Uint16Array(w * h * 4), attributes: new Uint32Array(w * h) } let post: ((buffer: unknown) => void) | undefined let layer: ((buffer: unknown) => void) | undefined const renderer = { addPostProcessFn: (fn: typeof post) => (post = fn), removePostProcessFn: () => (post = undefined), requestRender() {}, - getCursorState: () => ({ x: 10, y: 40, visible: true }), + getCursorState: () => ({ x: 10, y: h - 5, visible: true }), on() {}, off() {}, stdin: { on() {}, off() {} }, kittyImageTransport: "raw", root: {} as { onMouse?: unknown }, - ...(options.kitty ? { resolution: { width: W * 8, height: H * 16 }, capabilities: { kitty_graphics: true } } : {}), + ...(options.kitty ? { resolution: { width: w * 8, height: h * 16 }, capabilities: { kitty_graphics: true } } : {}), } const engine = createEngine({ renderer, data: { listen: () => () => {} } } as never, { layer: (_renderer, draw) => ((layer = draw as typeof layer), { dispose() {} }) }) return { @@ -31,10 +33,10 @@ export function mockScreen(options: { kitty?: boolean } = {}) { renderer, // OpenCode's render pass: the back layer places its image first, then the UI paints every cell, then post-processing. frame(paint: (x: number, y: number) => number = () => 32, buffer: Record = {}) { - const buf = { width: W, height: H, buffers, ...buffer } + const buf = { width: w, height: h, buffers, ...buffer } layer?.(buf) - for (let i = 0; i < W * H; i++) { - buffers.char[i] = paint(i % W, (i / W) | 0) + for (let i = 0; i < w * h; i++) { + buffers.char[i] = paint(i % w, (i / w) | 0) buffers.bg.set([10, 10, 12, 255], i * 4) buffers.fg.set([200, 200, 200, 255], i * 4) } @@ -45,3 +47,34 @@ export function mockScreen(options: { kitty?: boolean } = {}) { // Some text-like cells so the scrim and text handling get exercised. export const text = (x: number, y: number) => (y % 4 === 1 && x > 40 && x < 120 && x % 9 !== 0 ? 65 : 32) + +// Where text sits on OpenCode's home screen, measured in a real one: a centered logo, a 76-column prompt below it and +// the version in the bottom-right corner. +export function home(w: number, h: number) { + const c = Math.floor(h / 2) + const left = Math.floor(w / 2) - 37 + const spans: [number, number, number][] = [ + ...[0, 1, 2, 3].map((r): [number, number, number] => [c - 6 + r, left + 18, left + 58]), + [c - 1, left + 20, left + 56], + [c + 2, left + 3, left + 58], + [c + 4, left + 3, left + 32], + [c + 6, left + 42, left + 75], + ...[1, 2, 3, 4, 5].map((r): [number, number, number] => [c + r, left, left + 1]), + [h - 2, w - 17, w - 2], + ] + return (x: number, y: number) => (spans.some((s) => s[0] === y && x >= s[1] && x < s[2]) ? 65 : 32) +} + +// A session: a column of messages above the prompt, a status line, and from 120 columns a sidebar on the right. +export function session(w: number, h: number) { + const right = w >= 120 ? w - 42 : w - 2 + return (x: number, y: number) => { + if (y === h - 2) return x >= 2 && x < w - 2 && x % 23 < 15 ? 65 : 32 + if (x >= right + 2 && x < w - 2) return (y >= 2 && y < 4) || (y >= 6 && y < 15 && y % 3 === 0 && x < w - 16 - ((y * 7) % 12)) ? 65 : 32 + if (x < 2 || x >= right) return 32 + if (y >= h - 7 && y < h - 3) return y === h - 6 || (y === h - 4 && x < 30) ? 65 : 32 + if (y < 1 || y >= h - 8 || y % 6 === 5) return 32 + // Paragraphs of prose: every line fills the column except each paragraph's last. + return y % 6 !== 4 || x < 2 + (right - 2) * (0.3 + ((y * 37) % 50) / 100) ? 65 : 32 + } +} diff --git a/dev/preview.ts b/dev/preview.ts index 2835d1b..642d4c8 100644 --- a/dev/preview.ts +++ b/dev/preview.ts @@ -1,9 +1,11 @@ -// Renders what a wallpaper looks like in the terminal (octant cells behind some text) on a mock 160x45 screen, to a PNG: -// bun dev/preview.ts [calm|lively|teeming] [day|sunset|night] [spring|summer|autumn|winter] [clear|overcast|rain|snow|fog] [seconds] +// Renders what a wallpaper looks like in the terminal (octant cells behind some text) on a mock screen, to a PNG. home +// and session lay the text out like OpenCode's home screen or a session; a size such as 240x60 replaces 160x45: +// bun dev/preview.ts [calm|lively|teeming] [day|sunset|night] [spring|summer|autumn|winter] [clear|overcast|rain|snow|fog] [home|session] [WxH] [seconds] +import "./seed" import { mkdirSync } from "node:fs" import { ACTIVITIES, SEASONS, TIMES, WEATHERS } from "../src/engine" import { cellsToPng } from "./cells" -import { FPS, H, W, mockScreen, text } from "./mock" +import { FPS, H, W, home, mockScreen, session, text } from "./mock" import { wallpapers } from "./wallpapers" const [id, ...rest] = process.argv.slice(2) @@ -11,15 +13,19 @@ 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)) +const size = rest.find((arg) => /^\d+x\d+$/.test(arg)) +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]`) process.env.WALLPAPER_OCTANTS = "1" mkdirSync("dev/out", { recursive: true }) -const screen = mockScreen() +const screen = mockScreen({ W: w, H: h }) +const paint = layout === "home" ? home(w, h) : layout === "session" ? session(w, h) : text screen.engine.start(wallpaper, { activity, time, season, weather }) -for (let f = 0; f < Number(seconds) * FPS; f++) screen.frame(text) +for (let f = 0; f < Number(seconds) * FPS; f++) screen.frame(paint) screen.engine.stop() -const path = `dev/out/${[wallpaper.id, activity, time, season, weather, "preview", `${seconds}s`].filter(Boolean).join("-")}.png` -await Bun.write(path, cellsToPng({ W, H, ...screen.buffers })) +const path = `dev/out/${[wallpaper.id, activity, time, season, weather, layout, size, "preview", `${seconds}s`].filter(Boolean).join("-")}.png` +await Bun.write(path, cellsToPng({ W: w, H: h, ...screen.buffers })) console.log(path) diff --git a/src/api.ts b/src/api.ts index 1815efa..11353fd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -17,6 +17,7 @@ export { type Scrim, type Season, type Settings, + type TextMap, type Time, type Wallpaper, type Weather, diff --git a/src/canvas.ts b/src/canvas.ts index bd4e824..b891ddb 100644 --- a/src/canvas.ts +++ b/src/canvas.ts @@ -1,5 +1,5 @@ -import type { AgentEvent } from "./wallpaper" -import { clamp, type RGB } from "./math" +import type { AgentEvent, TextMap } from "./wallpaper" +import { clamp, rand, type RGB } from "./math" // Bloom is computed at 1/BLOOM resolution, then again at half that for the wide halo. const BLOOM = 4 @@ -72,6 +72,57 @@ export abstract class Canvas { this.gloom = clamp(this.gloom + (this.gloomy ? dt : -dt) * 0.3, 0, 1) } + // Where OpenCode's text sits; unknown until the engine says, and in dev/snap.ts. + protected text: TextMap | undefined + + textMap(map: TextMap) { + this.text = map + } + + // How hidden by text the point (x, y) in screen heights is, from 0 (open) to 1. + protected covered(x: number, y: number) { + const t = this.text + if (!t) return 0 + return t.cover[clamp(Math.floor(y * t.rows), 0, t.rows - 1) * t.cols + clamp(Math.floor((x / this.A) * t.cols), 0, t.cols - 1)] + } + + // A random point in the box from (x0, y0) to (x1, y1) in screen heights, the most open of a few tries once the + // engine has said where text is: creatures wander toward open ground and visitors come on where they'll be seen. + protected openSpot(x0: number, x1: number, y0: number, y1: number): [number, number] { + let best: [number, number] = [rand(x0, x1), rand(y0, y1)] + let least = this.covered(best[0], best[1]) + for (let i = 0; i < 5 && least > 0.05; i++) { + const spot: [number, number] = [rand(x0, x1), rand(y0, y1)] + const c = this.covered(spot[0], spot[1]) + if (c >= least) continue + best = spot + least = c + } + return best + } + + // A height between y0 and y1 for a visitor crossing the whole screen: the least covered row of a few tries. + protected openRow(y0: number, y1: number) { + let best = rand(y0, y1) + const t = this.text + if (!t) return best + const cover = (y: number) => { + const row = clamp(Math.floor(y * t.rows), 0, t.rows - 1) * t.cols + let sum = 0 + for (let c = 0; c < t.cols; c++) sum += t.cover[row + c] + return sum + } + let least = cover(best) + for (let i = 0; i < 4; i++) { + const y = rand(y0, y1) + const c = cover(y) + if (c >= least) continue + best = y + least = c + } + return best + } + // Called after every size change, once the buffers match the new size. protected layout() {} diff --git a/src/engine.tsx b/src/engine.tsx index 91a87a8..2e60cbc 100644 --- a/src/engine.tsx +++ b/src/engine.tsx @@ -4,7 +4,7 @@ import { appendFileSync } from "node:fs" import { watchEgg } from "./egg" import { lerp, smoothstep, type RGB } from "./math" import { OCTANTS } from "./octants" -import { AGENT_EVENTS, type AgentEvent, type Brightness, type Power, type Scene, type Scrim, type Settings, type Wallpaper } from "./wallpaper" +import { AGENT_EVENTS, type AgentEvent, type Brightness, type Power, type Scene, type Scrim, type Settings, type TextMap, type Wallpaper } from "./wallpaper" export { ACTIVITIES, @@ -22,6 +22,7 @@ export { type Scrim, type Season, type Settings, + type TextMap, type Time, type Wallpaper, type Weather, @@ -73,6 +74,10 @@ const LEVELS: Record> = { const DESATURATE = { day: 0.22, sunset: 0.22, night: 0.04 } // Seconds a change of wallpaper, activity or time of day crossfades over by default. export const FADE = 1.2 +// The map of where text sits that scenes get (see Scene.textMap): its grid, and how many frames apart it is rebuilt. +const MAP_COLS = 16 +const MAP_ROWS = 9 +const MAP_EVERY = 15 // Crossfades two equally sized RGBA frames into out. function mix(a: Uint8Array, b: Uint8Array, out: Uint8Array, t: number) { @@ -466,6 +471,10 @@ export function createEngine( let grid = new Float32Array(0) let gridBlur = new Float32Array(0) let light = false + const textMap: TextMap = { cols: MAP_COLS, rows: MAP_ROWS, cover: new Float32Array(MAP_COLS * MAP_ROWS) } + const mapCount = new Float32Array(MAP_COLS * MAP_ROWS) + let mapped = false + let mapFresh = false // Box-filters the scene down to the 2x4-per-cell octant grid, then applies an unsharp mask. const buildGrid = (px: Uint8Array, PW: number, PH: number, GW: number, GH: number) => { @@ -727,6 +736,11 @@ export function createEngine( // Blending in the previous frame fades at the same speed per second whatever the frame rate. const persistence = interval > 1000 / FPS ? PERSISTENCE ** (interval / (1000 / FPS)) : PERSISTENCE paint(buf, from ? mixed : scene.pixels, scene.W, scene.H, advanced, pixels, desaturate, shade, persistence, scrim) + if (mapFresh) { + mapFresh = false + scene.textMap?.(textMap) + from?.scene.textMap?.(textMap) + } if (options.dump && !dumped && frames > 120) { dumped = true write(buf, options.dump) @@ -816,6 +830,19 @@ export function createEngine( if (open.length !== cells) open = new Uint8Array(cells) openW = W for (let i = 0; i < cells; i++) open[i] = surface[i] && char[i] === SPACE && mask[((i / W) | 0) * 2 * W + (i % W)] < OPEN ? 1 : 0 + if (frames % MAP_EVERY === 1) { + const cover = textMap.cover + cover.fill(0) + mapCount.fill(0) + for (let i = 0; i < cells; i++) { + const g = Math.floor((((i / W) | 0) * MAP_ROWS) / H) * MAP_COLS + Math.floor(((i % W) * MAP_COLS) / W) + cover[g] += surface[i] ? Math.min(1, mask[((i / W) | 0) * 2 * W + (i % W)] * look.reach) : 1 + mapCount[g]++ + } + for (let g = 0; g < cover.length; g++) cover[g] /= Math.max(1, mapCount[g]) + mapped = true + mapFresh = true + } const cursor = renderer.getCursorState() const cursorX = cursor.visible ? cursor.x - 1 : -1 @@ -909,6 +936,7 @@ export function createEngine( const created = next.create(chosen) if (busy) created.react?.("busy") if (failed) created.react?.("error") + if (mapped) created.textMap?.(textMap) return created } diff --git a/src/wallpaper.ts b/src/wallpaper.ts index f0fbdea..c3eb8ee 100644 --- a/src/wallpaper.ts +++ b/src/wallpaper.ts @@ -16,6 +16,17 @@ export interface Scene { react?(event: AgentEvent): void // Optional reaction to a click on open background, in screen heights: x runs 0..W/H across and y 0..1 down. poke?(x: number, y: number): void + // Optional: where OpenCode's text sits, sent when the scene starts and about once a second after that, so creatures + // can keep to open ground and visitors can come on where they'll be seen. + textMap?(map: TextMap): void +} + +// A coarse grid over the whole screen, row-major: how much each cell is covered by text, its scrim or a panel, from 0 +// (open) to 1 (hidden). +export interface TextMap { + readonly cols: number + readonly rows: number + readonly cover: Float32Array } export const AGENT_EVENTS = ["busy", "idle", "done", "error"] as const diff --git a/wallpapers/beach.ts b/wallpapers/beach.ts index f977204..71a0b18 100644 --- a/wallpapers/beach.ts +++ b/wallpapers/beach.ts @@ -305,9 +305,10 @@ class Beach extends Canvas { if (season === "summer") for (const [i, [x, y, stripe]] of UMBRELLAS.slice(0, { calm: 2, lively: 4, teeming: 6 }[this.activity]).entries()) this.drawUmbrella(x * A, y, stripe, i % 3 === 2 ? undefined : SKIN[i % SKIN.length]) if (season === "autumn" || season === "winter") this.drawDriftwood(0.56 * A, 0.9) if (this.activity === "teeming" && season !== "autumn" && season !== "winter") this.drawBeachThings() + // The palms lean in from the edges, their crowns high and to the sides of the text in the middle. this.palms = [ - { base: [0.07 * A, 1.03], crown: [0.2 * A, 0.36], lean: 1 }, - { base: [0.94 * A, 1.0], crown: [0.85 * A, 0.44], lean: -1 }, + { base: [0.12, 1.03], crown: [0.35, 0.33], lean: 1 }, + { base: [A - 0.1, 1.0], crown: [A - 0.27, 0.39], lean: -1 }, ] this.background = this.hdr.slice() } @@ -534,8 +535,7 @@ class Beach extends Canvas { if (d.wait > 0) return d.t = 0 d.dir = Math.random() < 0.5 ? 1 : -1 - d.x = rand(0.3 * this.A, 0.7 * this.A) - d.y = rand(0.56, 0.66) + ;[d.x, d.y] = this.openSpot(0.2 * this.A, 0.8 * this.A, 0.56, 0.66) this.splash(d.x, d.y) return } diff --git a/wallpapers/city.ts b/wallpapers/city.ts index c2fb6c8..138cb28 100644 --- a/wallpapers/city.ts +++ b/wallpapers/city.ts @@ -335,7 +335,8 @@ class City extends Canvas { this.beacons = [] this.flickers = [] this.sign = undefined - // Hazy towers on the far side of the city, then the downtown skyline, tallest toward the middle. + // Hazy towers on the far side of the city, then the skyline, tallest in two districts toward the sides where text + // rarely covers them, and lower across the middle. for (let x = -0.03, i = 0; x < A + 0.03; i++) { const w = 0.035 + hash(i * 1.7 + 0.3) * 0.05 this.paintTower(this.tower(x, w, 0.06 + hash(i * 3.1 + 0.7) ** 1.5 * 0.17, i + 500, true)) @@ -344,8 +345,9 @@ class City extends Canvas { const near: Tower[] = [] for (let x = -0.02, i = 0; x < A + 0.02; i++) { const w = 0.05 + hash(i * 2.3 + 0.1) * 0.055 - const downtown = 0.13 * Math.exp(-(((x + w / 2 - 0.55 * A) / 0.4) ** 2)) - near.push(this.tower(x, w, 0.08 + hash(i * 4.7 + 0.2) ** 1.6 * 0.23 + downtown, i, false)) + const c = x + w / 2 + const downtown = 0.16 * Math.max(Math.exp(-(((c - 0.36) / 0.26) ** 2)), Math.exp(-(((c - A + 0.36) / 0.26) ** 2))) + near.push(this.tower(x, w, 0.08 + hash(i * 4.7 + 0.2) ** 1.6 * (0.16 + downtown * 0.45) + downtown, i, false)) x += w + (hash(i * 6.1) - 0.35) * 0.03 } // The tallest two get a spire and an antenna. diff --git a/wallpapers/desert.ts b/wallpapers/desert.ts index 570ddf3..bb15332 100644 --- a/wallpapers/desert.ts +++ b/wallpapers/desert.ts @@ -396,25 +396,26 @@ class Desert extends Canvas { }) if (this.season === "spring") this.drawBloom() if (this.activity === "teeming") { - for (const fx of [0.47, 0.92]) { - const gy = backTop[Math.min(W - 1, Math.round(fx * W))] + 2 - this.shadow(fx * A * H, gy, 0.05 * H) - this.drawSaguaro(fx * A * H, gy, (fx < 0.5 ? 0.14 : 0.11) * H, 0.5) + for (const [x, h] of [[0.45, 0.14], [A - 0.14, 0.11]]) { + const gy = backTop[clamp(Math.round(x * H), 0, W - 1)] + 2 + this.shadow(x * H, gy, 0.05 * H) + this.drawSaguaro(x * H, gy, h * H, 0.5) } } - this.shadow(0.13 * A * H, this.ground(0.13), 0.12 * H) - this.drawSaguaro(0.13 * A * H, this.ground(0.13) + 3, 0.34 * H, 0) - this.shadow(0.8 * A * H, this.ground(0.8), 0.08 * H) - this.drawSkull(0.8 * A * H, this.ground(0.8), 0.07 * H) + // The big saguaro and the skull keep to the sides, clear of the text in the middle. + this.shadow(0.22 * H, this.ground(0.22 / A), 0.12 * H) + this.drawSaguaro(0.22 * H, this.ground(0.22 / A) + 3, 0.34 * H, 0) + this.shadow((A - 0.32) * H, this.ground(1 - 0.32 / A), 0.08 * H) + this.drawSkull((A - 0.32) * H, this.ground(1 - 0.32 / A), 0.07 * H) if (this.activity === "teeming") { this.shadow(0.33 * A * H, this.ground(0.33), 0.05 * H) this.drawPricklyPear(0.33 * A * H, this.ground(0.33) + 2, H) this.shadow(0.62 * A * H, this.ground(0.62), 0.035 * H) this.drawBarrel(0.62 * A * H, this.ground(0.62) + 1, H) } - // The coyote sits on the tallest near mesa. - let best = 0 - for (let x = 1; x < W; x++) if (this.nearTop[x] < this.nearTop[best]) best = x + // The coyote sits on the tallest near mesa toward either side, where text rarely covers it, but not behind the saguaro. + let best = W - 1 - Math.round(W * 0.05) + for (let x = 0; x < W; x++) if (Math.abs(x / W - 0.5) > 0.3 && Math.abs(x / W - 0.5) < 0.47 && Math.abs(x / H - 0.22) > 0.1 && this.nearTop[x] < this.nearTop[best]) best = x this.coyoteX = best this.background = this.hdr.slice() } @@ -634,7 +635,7 @@ class Desert extends Canvas { s.wanderT -= dt if (s.wanderT <= 0 || Math.hypot(s.wx - s.x, s.wy - s.y) < 0.02) { s.wanderT = rand(6, 14) - s.wx = rand(0.3 * A, 0.7 * A) + s.wx = this.openSpot(0.3 * A, 0.7 * A, 0.9, 0.9)[0] s.wy = crest(s.wx) + rand(0.015, 0.06) } const dx = s.wx - s.x diff --git a/wallpapers/farm.ts b/wallpapers/farm.ts index 868c944..e6d2ebd 100644 --- a/wallpapers/farm.ts +++ b/wallpapers/farm.ts @@ -142,8 +142,6 @@ const CROWN: Record = { autumn: [[0.85, 0.42, 0.1], [[0.95, 0.68, 0.15], [0.7, 0.16, 0.06], [0.55, 0.3, 0.08]]], winter: [[0, 0, 0], []], } -// Where the two trees stand, as fractions of the width. -const TREES = [0.42, 0.95] const WOOD: RGB = [0.45, 0.3, 0.16] const BLACK: RGB = [0.05, 0.05, 0.05] @@ -188,6 +186,9 @@ class Farm extends Canvas { private readonly weather: WeatherLayer private hills: [far: RGB, near: RGB] private treeTops: [number, number][] = [] + // The barn and windmill stand toward the edges, in screen heights from the left, clear of the text in the middle. + private barnX = 0 + private millX = 0.25 // Blossom petals in spring and leaves in autumn, drifting down on the breeze. private leaves: { x: number; y: number; phase: number; color: RGB }[] = [] @@ -244,7 +245,7 @@ class Farm extends Canvas { this.stepTractor(dt) const A = this.A for (const c of this.cows) this.wander(c, cdt, [0.4 * A, 0.95 * A, 0.76, 0.87], 0.03, [6, 14]) - for (const c of this.chickens) this.wander(c, cdt, [0.62 * A, 0.84 * A, 0.672, 0.712], 0.025, [2, 5]) + for (const c of this.chickens) this.wander(c, cdt, [this.barnX - 0.21, this.barnX + 0.18, 0.672, 0.712], 0.025, [2, 5]) for (const p of this.pigs) this.wander(p, cdt, [0.44 * A, 0.6 * A, 0.88, 0.94], 0.02, [5, 10]) if (this.activity === "teeming") this.stepFarmer(cdt) this.stepEgg(dt, cdt) @@ -326,19 +327,20 @@ class Farm extends Canvas { } } if (season === "spring") this.drawWildflowers() - this.treeTops = TREES.map((t) => { - const base = this.nearTop[clamp(Math.round(t * W), 0, W - 1)] / H + 0.012 - this.drawTree(t * A, base) - return [t * A, base - 0.12] + this.barnX = A - 0.24 + this.treeTops = [0.5, A - 0.04].map((x) => { + const base = this.nearTop[clamp(Math.round(x * H), 0, W - 1)] / H + 0.012 + this.drawTree(x, base) + return [x, base - 0.12] }) - this.drawFarmhouse(0.5 * A, this.nearTop[clamp(Math.round(0.5 * W), 0, W - 1)] / H + 0.006) - this.drawWindmillTower(0.3 * A, 0.65) - this.drawBarn(0.74 * A, 0.655) - for (const [bx, by] of [[0.58, 0.705], [0.625, 0.712]]) this.shape([ell(bx * A * H, by * H, 0.022 * H, 0.02 * H, 0, this.paint([0.78, 0.62, 0.3]), 3)], this.lighting, 0.8) + this.drawFarmhouse(0.08, this.nearTop[clamp(Math.round(0.08 * H), 0, W - 1)] / H + 0.006) + this.drawWindmillTower(this.millX, 0.65) + this.drawBarn(this.barnX, 0.655) + for (const [dx, by] of [[-0.28, 0.705], [-0.2, 0.712]]) this.shape([ell((this.barnX + dx) * H, by * H, 0.022 * H, 0.02 * H, 0, this.paint([0.78, 0.62, 0.3]), 3)], this.lighting, 0.8) // Pumpkins in front of the barn in autumn. if (season === "autumn") for (const [dx, r] of [[-0.062, 0.011], [-0.046, 0.008], [0.056, 0.01]]) { - const [px, py] = [(0.74 * A + dx) * H, 0.664 * H] + const [px, py] = [(this.barnX + dx) * H, 0.664 * H] this.shape([ell(px, py - r * H, r * 1.25 * H, r * H, 0, this.paint([0.92, 0.45, 0.06]), 3), cap(px, py - r * 1.9 * H, px + 0.003 * H, py - r * 2.4 * H, 0.0018 * H, 0.0012 * H, this.paint([0.3, 0.35, 0.1]))], this.lighting, 0.7) } // A fence along the front of the pasture. @@ -461,7 +463,7 @@ class Farm extends Canvas { private drawFan() { const H = this.H - const x = 0.3 * this.A * H + const x = this.millX * H const y = (0.65 - 0.21) * H const metal = this.paint([0.75, 0.75, 0.72]) const turn = this.time * 0.8 @@ -570,8 +572,7 @@ class Farm extends Canvas { w.wanderT -= dt if (w.wanderT <= 0) { w.wanderT = rand(6, 14) - w.wx = rand(zone[0], zone[1]) - w.wy = rand(zone[2], zone[3]) + ;[w.wx, w.wy] = this.openSpot(...zone) } const dx = w.wx - w.x const dy = w.wy - w.y @@ -731,7 +732,7 @@ class Farm extends Canvas { for (let i = 0; i < 3; i++) { const a = this.creatureTime * (0.5 + i * 0.1) + i * 2.1 const x = (0.17 * this.A + Math.cos(a) * (0.08 + i * 0.03)) * H - const y = (0.42 + Math.sin(a) * 0.03) * H + const y = (0.3 + Math.sin(a) * 0.03) * H const flap = Math.sin(this.time * 6 + i * 2) const s = 0.03 * H const c = this.paint(BLACK) diff --git a/wallpapers/jungle.ts b/wallpapers/jungle.ts index 850f37c..ba7c5b2 100644 --- a/wallpapers/jungle.ts +++ b/wallpapers/jungle.ts @@ -13,6 +13,8 @@ const CHORUS = 3 const RAY_N = 1024 // How long the sloth's visit lasts, in creature time. const SLOTH = 24 +// Where the monkey hangs from the high branch, in screen heights from the right edge. +const MONKEY = 0.38 // What changes with the time of day. Plants and animals have one daytime color each; tint darkens them into // silhouettes at sunset and night, while the mist, distant trees, water and light shafts get their own colors. @@ -210,7 +212,7 @@ class Jungle extends Canvas { if (this.activity === "calm") return const m = this.monkey // Start the extra swing from rest so the push never makes it jump, swinging away from the click. - if (m.kick < 0.05) m.swing = x < this.rightBranch(0.71 * this.A)[0] / this.H ? Math.PI : 0 + if (m.kick < 0.05) m.swing = x < this.A - MONKEY ? Math.PI : 0 m.kick = 1 } @@ -222,8 +224,8 @@ class Jungle extends Canvas { if (this.sloth.t >= 0) this.drawSloth() if (this.activity !== "calm") this.drawMonkey() if (this.activity === "teeming") { - this.drawMacaw(0.795, -1, 0) - this.drawMacaw(0.765, 1, 1.7) + this.drawMacaw(this.A - 0.19, -1, 0) + this.drawMacaw(this.A - 0.24, 1, 1.7) this.drawJaguar() this.drawFrog() } @@ -250,8 +252,8 @@ class Jungle extends Canvas { this.lighting, 0.15, ) - // A rocky cliff with the waterfall pouring over it. - const fx = 0.55 * A + // A rocky cliff with the waterfall pouring over it, on the left; the animals' tree stands on the right. + const fx = 0.32 this.falls = { x: fx * H, top: 0.27 * H, bottom: 0.66 * H, half: (this.rainy ? 0.026 : 0.018) * H } const rock = mixRGB(this.paint(ROCK), look.far, 0.35) const boulders: [number, number, number, number][] = [ @@ -283,10 +285,10 @@ class Jungle extends Canvas { this.drawVine(vine.x * H, vine.length * H, vine.phase) } // Two big trunks with buttress roots, and the branches the animals use. - this.drawTrunk(0.2 * A * H, 0.045 * H) - this.drawTrunk(0.86 * A * H, 0.038 * H) - this.shape([cap(...this.leftBranch(0.2 * A + 0.03), ...this.leftBranch(0.48 * A), 0.022 * H, 0.012 * H, this.paint(TRUNK))], this.lighting) - this.shape([cap(...this.rightBranch(0.86 * A - 0.03), ...this.rightBranch(0.66 * A), 0.018 * H, 0.01 * H, this.paint(TRUNK))], this.lighting) + this.drawTrunk(0.04 * H, 0.045 * H) + this.drawTrunk((A - 0.12) * H, 0.038 * H) + this.shape([cap(...this.lowBranch(A - 0.15), ...this.lowBranch(A - 0.5), 0.022 * H, 0.012 * H, this.paint(TRUNK))], this.lighting) + this.shape([cap(...this.highBranch(A - 0.15), ...this.highBranch(A - 0.46), 0.018 * H, 0.01 * H, this.paint(TRUNK))], this.lighting) // The forest floor and its ferns. this.groundPx = new Float32Array(W) for (let x = 0; x < W; x++) this.groundPx[x] = (0.86 + 0.02 * Math.sin((x / H) * 3.3) + (fbm1(x * 0.02, 4) - 0.5) * 0.03) * H @@ -315,15 +317,14 @@ class Jungle extends Canvas { return [c[0] * t[0], c[1] * t[1], c[2] * t[2]] } - // Points along the two animal branches, in pixels, at x in screen heights. - private leftBranch(x: number): [number, number] { - const t = (x - (0.2 * this.A + 0.03)) / (0.28 * this.A - 0.03) - return [x * this.H, lerp(0.47, 0.43, t) * this.H] + // Points along the two animal branches of the right-hand tree, in pixels, at x in screen heights. They rise a little + // toward their tips. + private lowBranch(x: number): [number, number] { + return [x * this.H, lerp(0.69, 0.65, (this.A - 0.15 - x) / 0.35) * this.H] } - private rightBranch(x: number): [number, number] { - const t = ((0.86 * this.A - 0.03) - x) / (0.2 * this.A - 0.03) - return [x * this.H, lerp(0.31, 0.29, t) * this.H] + private highBranch(x: number): [number, number] { + return [x * this.H, lerp(0.31, 0.29, (this.A - 0.15 - x) / 0.31) * this.H] } // Misty depth: bright and hazy above, dark at the forest floor. @@ -445,7 +446,7 @@ class Jungle extends Canvas { if (b.next > 0) return b.dir = Math.random() < 0.5 ? 1 : -1 b.x = b.dir > 0 ? -0.15 : this.A + 0.15 - b.y = rand(0.18, 0.32) + b.y = this.openRow(0.16, 0.32) return } b.x += b.dir * 0.08 * dt @@ -480,10 +481,10 @@ class Jungle extends Canvas { ) } - // A spider monkey hanging by its tail from the right-hand branch, swinging slowly; at night it barely stirs. + // A spider monkey hanging by its tail from the high branch, swinging slowly; at night it barely stirs. private drawMonkey() { const H = this.H - const [ax, ay] = this.rightBranch(0.71 * this.A) + const [ax, ay] = this.highBranch(this.A - MONKEY) const m = this.monkey const swing = (this.look.night ? 0.05 : 0.35) * Math.sin(this.creatureTime * 1.2) + (this.look.night ? 0.2 : 0.4) * m.kick * Math.sin(m.swing) const cs = Math.cos(swing) @@ -543,10 +544,10 @@ class Jungle extends Canvas { this.shape([ell(...F(-0.035, 0.215), 0.028 * S, 0.012 * S, tilt - 0.35, dark), ell(...F(0.035, 0.215), 0.028 * S, 0.012 * S, tilt + 0.35, dark), ell(...F(0, 0.245), 0.014 * S, 0.008 * S, tilt, dark)], this.lighting, 0) } - // A scarlet macaw perched upright on the right-hand branch, bobbing its head now and then. + // A scarlet macaw perched upright on the high branch, bobbing its head now and then. private drawMacaw(at: number, face: number, phase: number) { const H = this.H - const [x, y] = this.rightBranch(at * this.A) + const [x, y] = this.highBranch(at) const S = 0.11 * H const bob = this.look.night ? 0 : Math.sin(this.creatureTime * 2 + phase) * 0.012 const P = (u: number, v: number): [number, number] => [x + u * face * S, y - 0.012 * H + v * S] @@ -568,20 +569,21 @@ class Jungle extends Canvas { this.add(...P(0.07, -0.7 + bob), 0.01, 0.01, 0.01) } - // A jaguar draped along the left-hand branch, legs dangling, breathing slowly as its tail sways. + // A jaguar draped along the low branch facing the trunk, legs dangling, breathing slowly as its tail sways. private drawJaguar() { - const { A, H } = this + const H = this.H + // x runs along its back from the tail end, in screen heights. const top = (x: number): [number, number] => { - const [px, py] = this.leftBranch(x * A) + const [px, py] = this.lowBranch(this.A - 0.46 + x) return [px, py - 0.035 * H] } const breathe = 1 + 0.03 * Math.sin(this.creatureTime * 1.5) const coat = this.paint([0.85, 0.58, 0.22]) - const [hx, hy] = top(0.445) - const [rx, ry] = top(0.29) - const [fx, fy] = top(0.415) + const [hx, hy] = top(0.235) + const [rx, ry] = top(0) + const [fx, fy] = top(0.19) const parts: Part[] = [ - cap(...top(0.29), ...top(0.41), 0.03 * H * breathe, 0.028 * H * breathe, coat), + cap(...top(0), ...top(0.18), 0.03 * H * breathe, 0.028 * H * breathe, coat), cap(fx, fy + 0.015 * H, fx + 0.004 * H, fy + 0.085 * H, 0.011 * H, 0.009 * H, coat), cap(fx - 0.02 * H, fy + 0.015 * H, fx - 0.018 * H, fy + 0.07 * H, 0.01 * H, 0.008 * H, coat), cap(rx + 0.01 * H, ry + 0.015 * H, rx + 0.012 * H, ry + 0.08 * H, 0.012 * H, 0.009 * H, coat), @@ -603,17 +605,17 @@ class Jungle extends Canvas { this.shape(parts, this.lighting, 0.7) // Rosettes along the body. for (let i = 0; i < 14; i++) { - const [sx, sy] = top(lerp(0.295, 0.405, hash(i * 3.3))) + const [sx, sy] = top(lerp(0.008, 0.174, hash(i * 3.3))) this.shape([ell(sx, sy + (hash(i * 7.1) - 0.5) * 0.035 * H, 0.0045 * H, 0.0035 * H, 0, this.paint([0.12, 0.07, 0.03]))], this.lighting, 0) } const glow = this.look.night ? 1.5 : 0 this.add(hx + 0.012 * H, hy - 0.008 * H, 0.8 * glow + 0.01, 1 * glow + 0.01, 0.3 * glow + 0.01) } - // A red-eyed tree frog on the left branch near the trunk, blinking now and then. + // A red-eyed tree frog on the high branch between the macaws and the monkey, blinking now and then. private drawFrog() { const H = this.H - const [x, y] = this.leftBranch(0.25 * this.A) + const [x, y] = this.highBranch(this.A - 0.3) const cy = y - 0.026 * H const green = this.paint([0.25, 0.7, 0.15]) const blink = this.time % 7 < 0.15 @@ -636,8 +638,7 @@ class Jungle extends Canvas { b.wanderT -= dt if (b.wanderT <= 0 || Math.hypot(b.wx - b.x, b.wy - b.y) < 0.03) { b.wanderT = rand(4, 9) - b.wx = rand(0.1 * A, 0.9 * A) - b.wy = rand(0.3, 0.75) + ;[b.wx, b.wy] = this.openSpot(0.1 * A, 0.9 * A, 0.3, 0.75) } const dx = b.wx - b.x const dy = b.wy - b.y diff --git a/wallpapers/ocean.ts b/wallpapers/ocean.ts index ec5e136..13b47bf 100644 --- a/wallpapers/ocean.ts +++ b/wallpapers/ocean.ts @@ -133,7 +133,8 @@ class Ocean extends Canvas { private sub = { x: -9, y: 0.4, dir: 1, wait: eggWait() * TIME_SCALE, spin: 0 } // Each creature keeps to its own home area so they never pile up: shark along the top, fish lower left, octopus - // lower right, diver upper left, turtle along the bottom. Areas are laid out before the first resize, at A = 1. + // lower right, diver upper left, turtle along the bottom, which leaves the middle, where text sits, to open water. + // Within its area each heads for the spots least covered by text. Areas are laid out before the first resize, at A = 1. private readonly activity: Activity private readonly timeOfDay: Time @@ -144,10 +145,10 @@ class Ocean extends Canvas { this.timeOfDay = settings.time if (activity === "teeming") this.fish.push(this.makeFish("ambient:shark", "shark", [0.3, 1, 0.17, 0.33], { len: 0.64, z: 0.55 })) if (activity !== "calm") { - this.fish.push(this.makeFish("ambient:fish", "fish", [0, 0.44, 0.48, 0.72], { len: 0.26, z: 0.35, hue: 0.07, sat: 0.85 })) - const zone: Zone = [0.58, 1, 0.46, 0.7] + this.fish.push(this.makeFish("ambient:fish", "fish", [0, 0.36, 0.5, 0.76], { len: 0.26, z: 0.35, hue: 0.07, sat: 0.85 })) + const zone: Zone = [0.74, 1, 0.54, 0.76] const ox = ((zone[0] + zone[1]) / 2) * this.A - this.octopus = { x: ox, y: 0.58, z: 0.4, vx: 0, vy: 0, size: 0.1, hue: 0.02, phase: 0, pulse: 0, next: 2, dir: -Math.PI / 2, wx: ox, wy: 0.58, zone } + this.octopus = { x: ox, y: 0.64, z: 0.4, vx: 0, vy: 0, size: 0.1, hue: 0.02, phase: 0, pulse: 0, next: 2, dir: -Math.PI / 2, wx: ox, wy: 0.64, zone } } if (activity === "teeming") { this.turtle = { x: 0.5 * this.A, y: 0.84, z: 0.4, vx: 0, vy: 0, face: -1, turn: -1, heading: -1, phase: 0, wx: 0.5 * this.A, wy: 0.84, wanderT: 0, zone: [0.22, 0.8, 0.78, 0.9] } @@ -213,8 +214,7 @@ class Ocean extends Canvas { tu.wanderT -= dt if (tu.wanderT <= 0 || Math.hypot(tu.wx - tu.x, tu.wy - tu.y) < 0.04) { tu.wanderT = rand(8, 14) - tu.wx = rand(zx0, zx1) - tu.wy = rand(tu.zone[2], tu.zone[3]) + ;[tu.wx, tu.wy] = this.openSpot(zx0, zx1, tu.zone[2], tu.zone[3]) } tu.phase += dt * 1.5 const dx = tu.wx - tu.x @@ -235,8 +235,7 @@ class Ocean extends Canvas { dv.wanderT -= dt if (dv.wanderT <= 0 || Math.hypot(dv.wx - dv.x, dv.wy - dv.y) < 0.03) { dv.wanderT = rand(5, 10) - dv.wx = rand(zx0, zx1) - dv.wy = rand(dv.zone[2], dv.zone[3]) + ;[dv.wx, dv.wy] = this.openSpot(zx0, zx1, dv.zone[2], dv.zone[3]) } const dx = dv.wx - dv.x const dy = dv.wy - dv.y @@ -262,8 +261,7 @@ class Ocean extends Canvas { o.next -= dt if (o.next <= 0) { if (Math.hypot(o.wx - o.x, o.wy - o.y) < 0.12 || Math.random() < 0.3) { - o.wx = rand(o.zone[0] * this.A + 0.12, o.zone[1] * this.A - 0.12) - o.wy = rand(o.zone[2] + 0.04, o.zone[3] - 0.04) + ;[o.wx, o.wy] = this.openSpot(o.zone[0] * this.A + 0.12, o.zone[1] * this.A - 0.12, o.zone[2] + 0.04, o.zone[3] - 0.04) } const dx = o.wx - o.x const dy = o.wy - o.y @@ -423,8 +421,7 @@ class Ocean extends Canvas { if (f.wanderT <= 0 || Math.hypot(f.wx - f.x, f.wy - f.y) < 0.05) { f.wanderT = rand(3, 8) const m = f.len * this.zScale(f) * 0.5 - f.wx = rand(f.zone[0] * this.A + m, Math.max(f.zone[0] * this.A + m, f.zone[1] * this.A - m)) - f.wy = rand(f.zone[2], f.zone[3]) + ;[f.wx, f.wy] = this.openSpot(f.zone[0] * this.A + m, Math.max(f.zone[0] * this.A + m, f.zone[1] * this.A - m), f.zone[2], f.zone[3]) } const dx = tx - f.x const dy = ty - f.y @@ -474,7 +471,7 @@ class Ocean extends Canvas { if (w.next > 0) return w.dir = Math.random() < 0.5 ? 1 : -1 w.x = w.dir > 0 ? -0.9 : this.A + 0.9 - w.y = rand(0.3, 0.48) + w.y = this.openRow(0.22, 0.48) return } w.x += w.dir * 0.055 * dt @@ -491,7 +488,7 @@ class Ocean extends Canvas { if (s.wait > 0) return s.dir = Math.random() < 0.5 ? 1 : -1 s.x = s.dir > 0 ? -0.3 : this.A + 0.3 - s.y = rand(0.32, 0.42) + s.y = this.openRow(0.28, 0.44) return } s.x += s.dir * 0.1 * cdt diff --git a/wallpapers/prehistoric.ts b/wallpapers/prehistoric.ts index d37610a..8ab13d8 100644 --- a/wallpapers/prehistoric.ts +++ b/wallpapers/prehistoric.ts @@ -11,8 +11,8 @@ const TIME_SCALE = 0.35 // Animals move at a quarter of scene speed, so even the T. rex plods. const CREATURE_SPEED = 0.25 const HORIZON = 0.6 -// The volcano's crater, in screen heights; its x is a fraction of the width. -const VENT_X = 0.7 +// The volcano's crater, in screen heights: its x from the right edge, so it stays clear of the text in the middle. +const VENT_X = 0.4 const CRATER = 0.205 // Smoke puffs leave the crater on a loop: each one is the same plume at a different age. const SMOKE_LIFE = 14 @@ -277,7 +277,7 @@ class Prehistoric extends Canvas { this.lighting = look.style === "front" ? { style: "front", dir: DAYLIGHT } : { style: "rim", color: look.light, x: look.orb.x * W, y: look.orb.y * H } paintSky(this.hdr, W, H, look.sky, look.orb) this.weather.cover(this.hdr, W, H) - const vx = VENT_X * A + const vx = A - VENT_X const ridgeTop = new Float32Array(W) const volcanoTop = new Float32Array(W) const forestTop = new Float32Array(W) @@ -323,10 +323,11 @@ class Prehistoric extends Canvas { return this.rim([lerp(far[0], near[0], depth) * blades, lerp(far[1], near[1], depth) * blades, lerp(far[2], near[2], depth) * blades], x, d, 2) }) if (this.activity === "teeming") this.paintLake() - this.trees = [0.08 * A, Math.max(0.4 * A, 0.08 * A + 0.45)] + // The sauropod's two trees keep to the left, the volcano to the right. + this.trees = [0.06, 0.48] this.drawTree(this.trees[0], 0.665, 0.29) this.drawTree(this.trees[1], 0.662, 0.31) - this.drawTree(0.93 * A, 0.64, 0.45) + this.drawTree(A - 0.1, 0.64, 0.45) for (const [i, [x, y, s]] of [ [0.32, 0.78, 0.07], [0.88, 0.74, 0.08], @@ -443,7 +444,7 @@ class Prehistoric extends Canvas { const k = 0.55 + 0.1 * Math.sin(y * 2.1 + x * 0.05) this.blend(o, lerp(look.water[0], this.hdr[m], k), lerp(look.water[1], this.hdr[m + 1], k), lerp(look.water[2], this.hdr[m + 2], k), wet) // The volcano's glow shimmers on the water below it. - const sheen = look.glow * 0.25 * Math.exp(-Math.abs(x / H - VENT_X * A) / 0.1) * wet * (0.7 + 0.3 * Math.sin(y * 2.7)) + const sheen = look.glow * 0.25 * Math.exp(-Math.abs(x / H - (A - VENT_X)) / 0.1) * wet * (0.7 + 0.3 * Math.sin(y * 2.7)) this.hdr[o] += red[0] * sheen this.hdr[o + 1] += red[1] * sheen this.hdr[o + 2] += red[2] * sheen @@ -452,7 +453,7 @@ class Prehistoric extends Canvas { // The lake's center and radii, in screen heights. private lake() { - return [0.66 * this.A, 0.735, Math.min(0.3, 0.22 * this.A), 0.042] as const + return [this.A - VENT_X - 0.1, 0.735, Math.min(0.3, 0.22 * this.A), 0.042] as const } // A monkey-puzzle conifer: a bare trunk with tiers of drooping branches toward its rounded top. @@ -530,7 +531,7 @@ class Prehistoric extends Canvas { const [hot, cool, alpha] = look.smoke for (const s of this.smoke) { const t = s.age - const x = (VENT_X * A + (hash(s.seed) - 0.5) * 0.02 + 0.002 * t + 0.0009 * t * t + 0.006 * Math.sin(t * 0.7 + s.seed)) * H + const x = (A - VENT_X + (hash(s.seed) - 0.5) * 0.02 + 0.002 * t + 0.0009 * t * t + 0.006 * Math.sin(t * 0.7 + s.seed)) * H const y = (CRATER - 0.004 - 0.018 * t + 0.0004 * t * t) * H const r = (0.012 + 0.006 * t) * (0.8 + hash(s.seed + 1) * 0.4) * H const a = alpha * smoothstep(0, 1.5, t) * (1 - smoothstep(8, SMOKE_LIFE, t)) @@ -797,8 +798,7 @@ class Prehistoric extends Canvas { w.wanderT -= dt if (w.wanderT <= 0) { w.wanderT = rand(6, 14) - w.wx = rand(zone[0], zone[1]) - w.wy = rand(zone[2], zone[3]) + ;[w.wx, w.wy] = this.openSpot(...zone) } const dx = w.wx - w.x const dy = w.wy - w.y @@ -890,8 +890,7 @@ class Prehistoric extends Canvas { if (n.wait > 0) return const [cx, cy, rx, ry] = this.lake() n.t = 0 - n.x = cx + rand(-0.6, 0.6) * rx - n.y = cy + rand(-0.2, 0.4) * ry + ;[n.x, n.y] = this.openSpot(cx - 0.6 * rx, cx + 0.6 * rx, cy - 0.2 * ry, cy + 0.4 * ry) n.dir = Math.random() < 0.5 ? 1 : -1 return } diff --git a/wallpapers/space.ts b/wallpapers/space.ts index 6c39a47..e11c155 100644 --- a/wallpapers/space.ts +++ b/wallpapers/space.ts @@ -11,8 +11,9 @@ const CREATURE_SPEED = 0.25 const TW = 512 const TH = 256 -// Where the planet sits and how big it is: center x as a fraction of the width, y and radius in screen heights. -const PLANET = { x: 0.68, y: 1.1, r: 0.58 } +// Where the planet sits and how big it is, in screen heights: its center x from the right edge, so it rises in the +// lower right clear of the text in the middle, and its center y and radius. +const PLANET = { x: 0.52, y: 1.1, r: 0.58 } const RING = { tilt: 0.42, flat: 0.24, inner: 1.25, outer: 2.1 } // How long a shooting star lasts, in scene seconds. const METEOR = 1.2 @@ -128,7 +129,7 @@ class Space extends Canvas { private stars = Array.from({ length: 320 }, (_, i) => ({ x: Math.random() * 3, y: Math.random(), layer: i % 3, b: 0.3 + Math.random() * 0.7, phase: Math.random() * TAU, tint: STAR_TINTS[i % STAR_TINTS.length] })) private comet = { x: -9, y: 0, vx: 0, vy: 0, next: 14 } private ufo = { x: -9, y: 0.2, dir: 1, next: 30 } - private station: Traveler = { x: 0.2, y: 0.3, dir: 1, wait: 0 } + private station: Traveler = { x: 0.2, y: 0.24, dir: 1, wait: 0 } private satellite: Traveler = { x: 1.4, y: 0.15, dir: -1, wait: 0 } private rocks: Rock[] = [] private meteors: { x: number; y: number; vx: number; vy: number; age: number }[] = [] @@ -237,7 +238,7 @@ class Space extends Canvas { // The nebula, the Milky Way and the far half of the rings never move, so they are painted once per size. protected override layout() { const { W, H, A, look } = this - const cx = PLANET.x * W + const cx = W - PLANET.x * H const cy = PLANET.y * H const R = PLANET.r * H this.starPos = look.lighting === "front" ? [0.14 * W, 0.14 * H] : [cx + R * look.sun[0] / Math.hypot(look.sun[0], look.sun[1]), cy + R * look.sun[1] / Math.hypot(look.sun[0], look.sun[1])] @@ -324,7 +325,7 @@ class Space extends Canvas { // the edge, and on the dark side city lights and aurora. private drawPlanet() { const { W, H, hdr, look } = this - const cx = PLANET.x * W + const cx = W - PLANET.x * H const cy = PLANET.y * H const R = PLANET.r * H const rot = this.time * 0.02 @@ -422,8 +423,8 @@ class Space extends Canvas { private drawMoon() { const { W, H, look } = this const a = this.creatureTime * 0.05 - const cx = (0.24 * this.A + 0.06 * Math.cos(a)) * H - const cy = (0.36 + 0.025 * Math.sin(a)) * H + const cx = (0.3 + 0.06 * Math.cos(a)) * H + const cy = (0.24 + 0.025 * Math.sin(a)) * H const R = 0.035 * H for (let y = Math.max(0, Math.floor(cy - R - 1)); y <= Math.min(H - 1, cy + R + 1); y++) for (let x = Math.max(0, Math.floor(cx - R - 1)); x <= Math.min(W - 1, cx + R + 1); x++) { @@ -604,7 +605,7 @@ class Space extends Canvas { private drawAstronaut() { const H = this.H const t = this.creatureTime - const x = (0.24 * this.A + 0.05 * Math.sin(t * 0.13)) * H + const x = (0.28 + 0.05 * Math.sin(t * 0.13)) * H const y = (0.6 + 0.03 * Math.sin(t * 0.21)) * H const a = t * 0.08 const S = 0.09 * H diff --git a/wallpapers/tundra.ts b/wallpapers/tundra.ts index f7d1dd3..58be994 100644 --- a/wallpapers/tundra.ts +++ b/wallpapers/tundra.ts @@ -159,7 +159,7 @@ class Tundra extends Canvas { this.flakes = Array.from({ length: settings.weather ? 0 : { calm: 70, lively: 100, teeming: 130 }[settings.activity] }, () => ({ x: Math.random() * 4, y: Math.random(), z: Math.random(), s: Math.random() })) const walker = (x: number, y: number): Walker => ({ x, y, wx: x, wy: y, wanderT: 0, face: 1, phase: Math.random() * TAU, rest: 0, moving: false, hop: Infinity }) if (settings.activity !== "calm") { - this.penguins = Array.from({ length: settings.activity === "teeming" ? 5 : 3 }, (_, i) => walker(0.6 + i * 0.12, 0.78 + (i % 2) * 0.015)) + this.penguins = Array.from({ length: settings.activity === "teeming" ? 5 : 3 }, (_, i) => walker(0.6 + i * 0.12, 0.81 + (i % 2) * 0.015)) this.fox = walker(0.9, 0.88) } if (settings.activity === "teeming") this.bear = walker(0.7, 0.655) @@ -185,7 +185,7 @@ class Tundra extends Canvas { } this.stepOwl(dt) const A = this.A - for (const p of this.penguins) this.wander(p, cdt, [0.34 * A, 0.58 * A, 0.77, 0.81], 0.025) + for (const p of this.penguins) this.wander(p, cdt, [0.34 * A, 0.58 * A, 0.8, 0.84], 0.025) if (this.fox) this.wander(this.fox, cdt, [0.3 * A, 0.95 * A, 0.84, 0.92], 0.04) if (this.bear) this.wander(this.bear, cdt, [0.25 * A, 0.85 * A, 0.648, 0.662], 0.025) const f = this.fisher @@ -201,7 +201,7 @@ class Tundra extends Canvas { y.wait -= dt if (y.wait > 0) return y.t = 0 - y.x = rand(0.56, 0.6) * this.A + y.x = this.openSpot(0.15 * this.A, 0.8 * this.A, 0.6, 0.6)[0] return } y.t += cdt @@ -260,10 +260,12 @@ class Tundra extends Canvas { const edge = look.style === "rim" ? Math.exp(-(y + 0.5 - drift[x]) / 2) * 0.3 : 0 this.blend((y * W + x) * 3, c[0] + look.light[0] * edge, c[1] + look.light[1] * edge, c[2] + look.light[2] * edge, clamp(y + 1 - drift[x], 0, 1)) } - this.drawLake(0.5 * A, 0.72, 0.3, 0.045) - for (const [fx, h] of [[0.3, 0.1], [0.36, 0.08], [0.7, 0.09], [0.76, 0.11]] as const) this.drawPine(fx * A, 0.625, h, 0.5) - this.drawIgloo(0.86 * A, 0.7, 0.055) - this.drawIgloo(0.2 * A, 0.75, 0.075) + this.drawLake(0.5 * A, 0.745, 0.3, 0.045) + // The pines and igloos keep to the sides, clear of the text in the middle. + for (const [x, h] of [[0.3, 0.1], [0.37, 0.08], [A - 0.36, 0.09], [A - 0.29, 0.11]]) this.drawPine(x, 0.625, h, 0.5) + this.drawIgloo(A - 0.2, 0.7, 0.055) + this.drawIgloo(A - 0.36, 0.77, 0.08) + this.drawIgloo(0.42, 0.79, 0.075) if (this.activity === "teeming") this.drawSnowman(0.3 * A, 0.85, 0.75, [0.2, 0.35, 0.8], false) this.drawSnowman(0.62 * A, 0.87, 1, [0.8, 0.12, 0.1], true) for (const [fx, h] of [[0.04, 0.46], [0.11, 0.34], [0.955, 0.42], [0.995, 0.3]] as const) this.drawPine(fx * A, 1.02, h, 0) @@ -450,8 +452,7 @@ class Tundra extends Canvas { w.wanderT -= dt if (w.wanderT <= 0) { w.wanderT = rand(6, 14) - w.wx = rand(zone[0], zone[1]) - w.wy = rand(zone[2], zone[3]) + ;[w.wx, w.wy] = this.openSpot(...zone) } const dx = w.wx - w.x const dy = w.wy - w.y @@ -588,7 +589,7 @@ class Tundra extends Canvas { const H = this.H const S = 0.12 * H const hx = 0.68 * this.A * H - const hy = 0.718 * H + const hy = 0.758 * H const x = hx + 0.06 * H const y = hy const P = (u: number, v: number): [number, number] => [x + u * S, y + v * S] @@ -621,7 +622,7 @@ class Tundra extends Canvas { if (o.next > 0) return o.dir = Math.random() < 0.5 ? 1 : -1 o.x = o.dir > 0 ? -0.15 : this.A + 0.15 - o.y = rand(0.2, 0.35) + o.y = this.openRow(0.18, 0.35) return } o.x += o.dir * 0.07 * dt diff --git a/wallpapers/zen.ts b/wallpapers/zen.ts index c1f2340..ed48ee0 100644 --- a/wallpapers/zen.ts +++ b/wallpapers/zen.ts @@ -481,7 +481,7 @@ class Zen extends Canvas { this.blend((y * W + x) * 3, c[0], c[1], c[2], clamp(y + 1 - top, 0, 1)) } } - this.drawPagoda(0.71 * A, 0.482) + this.drawPagoda(A - 0.4, 0.482) } // A three-tiered pagoda on the far ridge, hazed by distance. @@ -1192,8 +1192,7 @@ class Zen extends Canvas { const l = Math.hypot(dx, dy) if (l < 0.004) { d.rest = rand(2, 5) - d.tx = this.pond.x + rand(-0.9, 0.9) * this.pond.rx - d.ty = rand(0.6, 0.82) + ;[d.tx, d.ty] = this.openSpot(this.pond.x - 0.9 * this.pond.rx, this.pond.x + 0.9 * this.pond.rx, 0.6, 0.82) return } if (Math.abs(dx) > 0.002) d.face = Math.sign(dx)