diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c067710..3f9ddef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,11 @@ details), and values in between soften it. - `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`. +`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. diff --git a/src/api.ts b/src/api.ts index 1815efa..442c6e8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,6 +2,7 @@ export { Canvas, cap, ell, type Lighting, type Part } from "./canvas" export { eggWait } from "./egg" export { bird, flyAway, startle, type Flier } from "./flock" +export { haze, mottle } from "./grade" export { TAU, clamp, fbm1, fbm2, hash, hash2, hashString, hsv, lerp, noise1, noise2, rand, smoothstep, type RGB } from "./math" export { STAR_TINTS, driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "./sky" export { diff --git a/src/canvas.ts b/src/canvas.ts index bd4e824..4877963 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 { clamp, smoothstep, type RGB } from "./math" // Bloom is computed at 1/BLOOM resolution, then again at half that for the wide halo. const BLOOM = 4 @@ -56,6 +56,9 @@ export abstract class Canvas { // clouds by it. protected gloom = 0 private gloomy = false + // Extra darkening toward the bottom corners (0 to 1), folded into the vignette: set it in the constructor to frame + // a bright daytime foreground. + protected frame = 0 abstract step(dt: number): void abstract render(): void @@ -99,7 +102,8 @@ export abstract class Canvas { for (let x = 0; x < W; x++) { const dx = (x - W / 2) * invR const dy = (y - H / 2) * invR - this.vignette[y * W + x] = 1 - 0.62 * Math.pow(dx * dx + dy * dy, 1.25) + const corner = this.frame * smoothstep(0.45, 1, y / H) * (0.35 + 0.65 * (2 * Math.abs(x / W - 0.5)) ** 2) + this.vignette[y * W + x] = (1 - 0.62 * Math.pow(dx * dx + dy * dy, 1.25)) * (1 - corner) } // Bilinear lookup from full-resolution rows and columns into the bloom grid. this.rowA = new Int32Array(H) diff --git a/src/grade.ts b/src/grade.ts new file mode 100644 index 0000000..064a00e --- /dev/null +++ b/src/grade.ts @@ -0,0 +1,37 @@ +// Daytime depth for static backgrounds, applied once in layout(): aerial haze and broad ground patches. Rows are +// fractions of the height. +import { fbm2, lerp, smoothstep, type RGB } from "./math" + +// Aerial perspective: whatever was painted between rows `top` and y1 fades toward the sky behind it, by `far` down to +// row y0 (the horizon), easing to `near` at y1. `sky` is a copy of hdr taken right after the sky (and weather) was +// painted, so open sky is unchanged and distant shapes melt into the exact color around them. Below the horizon the +// sky copy holds the horizon color, which is what distant ground fades to. Call it after each layer to haze the +// farther ones more. +export function haze(hdr: Float32Array, sky: Float32Array, W: number, H: number, y0: number, y1: number, far: number, near = 0, top = 0) { + for (let y = Math.max(0, Math.floor(top * H)); y < Math.min(H, Math.ceil(y1 * H)); y++) { + const k = lerp(far, near, smoothstep(y0, y1, (y + 0.5) / H)) + if (k <= 0) continue + for (let o = y * W * 3; o < (y + 1) * W * 3; o++) hdr[o] += (sky[o] - hdr[o]) * k + } +} + +// Gentle, broad tone patches on the ground from the horizon at y0 down to y1: low-frequency noise laid out in +// perspective (patches flatten and shrink toward the horizon, where they fade out), tinting toward `warm` on one side +// and `cool` on the other. Both are multipliers near 1; `size` is a patch's width at the bottom, in screen heights. +export function mottle(hdr: Float32Array, W: number, H: number, y0: number, y1: number, size: number, warm: RGB, cool: RGB) { + const span = 1 - y0 + 0.04 + for (let y = Math.max(0, Math.ceil(y0 * H)); y < Math.min(H, Math.ceil(y1 * H)); y++) { + const v = (y + 0.5) / H + const z = Math.min(6, span / (v - y0 + 0.04)) + const fade = smoothstep(y0, y0 + 0.08, v) + for (let x = 0; x < W; x++) { + const n = Math.max(-1, Math.min(1, (fbm2(((x / H) * z) / size, (z * span) / size, 3) - 0.5) * 3.2)) * fade + const t = n > 0 ? warm : cool + const a = Math.abs(n) + const o = (y * W + x) * 3 + hdr[o] *= 1 + (t[0] - 1) * a + hdr[o + 1] *= 1 + (t[1] - 1) * a + hdr[o + 2] *= 1 + (t[2] - 1) * a + } + } +} diff --git a/wallpapers/beach.ts b/wallpapers/beach.ts index f977204..074a1bf 100644 --- a/wallpapers/beach.ts +++ b/wallpapers/beach.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { bird, flyAway, startle, type Flier } from "../src/flock" import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" @@ -44,12 +45,12 @@ const LOOKS: Record = { stars: 0, clouds: { puffy: true, top: [1.1, 1.1, 1.1], bottom: [0.6, 0.68, 0.8], alpha: 0.9 }, sea: [ - [0.12, 0.42, 0.62], - [0.08, 0.6, 0.62], + [0.04, 0.26, 0.56], + [0.03, 0.54, 0.6], ], sand: [ - [0.86, 0.76, 0.55], - [0.62, 0.52, 0.36], + [0.8, 0.65, 0.43], + [0.5, 0.4, 0.27], ], foam: [0.92, 0.96, 1], glitter: 0.15, @@ -190,6 +191,7 @@ class Beach extends Canvas { const look = offSeason && settings.time === "day" ? { ...LOOKS.day, ...OFF_SEASON } : LOOKS[settings.time] // With the sun or moon behind cloud, only a trace of the glitter path is left. this.look = this.weather.covered ? { ...look, glitter: look.glitter * 0.15 } : look + if (settings.time === "day") this.frame = 0.4 if (this.season === "summer" && !this.look.night) this.swimmers = Array.from({ length: { calm: 1, lively: 3, teeming: 5 }[settings.activity] }, (_, i) => ({ x: 0.25 + i * 0.13 + hash(i) * 0.05, y: 0.69 + hash(i * 3.3) * 0.03, phase: i * 1.9 })) this.stars = makeStars(this.look.stars, 0.45) this.clouds = makeClouds(this.look.clouds.puffy ? 4 : 5, this.look.clouds.puffy, 0.08, 0.3) @@ -282,6 +284,7 @@ class Beach 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 sky = this.hdr.slice() // A small island on the horizon. const ix = 0.16 * A this.polygon(Array.from({ length: 21 }, (_, i) => [(ix + Math.cos((i / 20) * Math.PI) * 0.08) * H, (HORIZON - Math.sin((i / 20) * Math.PI) * 0.025) * H] as const), this.paint([0.2, 0.35, 0.22])) @@ -300,6 +303,11 @@ class Beach extends Canvas { this.hdr[o + 1] = dry[1] * ripple this.hdr[o + 2] = dry[2] * ripple } + // By day the island fades into the sky, and the sand drifts between warm and cool. + if (look.style === "front") { + haze(this.hdr, sky, W, H, HORIZON, HORIZON + 0.01, 0.4) + mottle(this.hdr, W, H, HORIZON, 1, 0.6, [1.12, 1, 0.8], [0.72, 0.78, 0.92]) + } // Summer brings a crowd of umbrellas and sunbathers; out of season the beach is empty but for driftwood. const season = this.season 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]) @@ -329,6 +337,7 @@ class Beach extends Canvas { const t = this.time const [far, near] = look.sea const [, wet] = look.sand + const sky = look.sky[look.sky.length - 1][1] const ox = look.orb.x * W const roll = (t * 0.08) % 1 for (let x = 0; x < W; x++) { @@ -347,9 +356,11 @@ class Beach extends Canvas { continue } const depth = clamp((y / H - HORIZON) / (edge / H - HORIZON), 0, 1) - let r = lerp(far[0], near[0], depth) - let g = lerp(far[1], near[1], depth) - let b = lerp(far[2], near[2], depth) + // By day the far water pales into the haze along the horizon. + const mist = look.style === "front" ? (1 - depth) ** 4 * 0.5 : 0 + let r = lerp(lerp(far[0], near[0], depth), sky[0], mist) + let g = lerp(lerp(far[1], near[1], depth), sky[1], mist) + let b = lerp(lerp(far[2], near[2], depth), sky[2], mist) const streak = Math.sin(u * (30 - depth * 18) + y * 0.9 + t * 0.6 + Math.sin(y * 0.37) * 3) const shine = streak > 0.9 ? (streak - 0.9) * 2 * (0.4 + depth) : 0 r += look.light[0] * shine * 0.25 diff --git a/wallpapers/city.ts b/wallpapers/city.ts index c2fb6c8..a860e1d 100644 --- a/wallpapers/city.ts +++ b/wallpapers/city.ts @@ -1,6 +1,7 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" import { bird, flyAway, startle, type Flier } from "../src/flock" +import { haze } from "../src/grade" import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" @@ -55,10 +56,10 @@ const LOOKS: Record = { orb: { x: 0.14, y: 0.11, r: 0.035, core: [5, 4.6, 3.8], glow: [1, 0.95, 0.8], near: 0.45, wide: 0.12 }, stars: 0, clouds: { puffy: true, top: [1.1, 1.1, 1.1], bottom: [0.6, 0.66, 0.78], alpha: 0.85 }, - haze: [0.55, 0.65, 0.78], + haze: [0.48, 0.6, 0.8], river: [ - [0.2, 0.36, 0.48], - [0.08, 0.2, 0.3], + [0.12, 0.3, 0.48], + [0.05, 0.16, 0.3], ], reflect: 0.55, tint: [1, 1, 1], @@ -124,12 +125,12 @@ const DAYLIGHT = (() => { // Daytime colors. const FACADES: RGB[] = [ - [0.5, 0.56, 0.64], - [0.68, 0.62, 0.52], - [0.32, 0.46, 0.6], - [0.58, 0.42, 0.36], - [0.4, 0.52, 0.54], - [0.62, 0.62, 0.68], + [0.3, 0.42, 0.6], + [0.7, 0.55, 0.4], + [0.18, 0.33, 0.55], + [0.62, 0.36, 0.26], + [0.22, 0.43, 0.48], + [0.66, 0.6, 0.53], ] const PAINT: RGB[] = [ [0.75, 0.12, 0.1], @@ -248,6 +249,7 @@ class City extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.4 this.stars = makeStars(this.look.stars, 0.45) this.clouds = makeClouds(this.look.clouds.puffy ? 3 : 4, this.look.clouds.puffy, 0.04, 0.1) this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, BANK) @@ -353,6 +355,7 @@ class City extends Canvas { tallest[0].crown = 3 tallest[1].crown = 2 for (const t of near) this.paintTower(t) + if (look === LOOKS.day) haze(this.hdr, sky, W, H, BANK, BANK + 0.01, 0.12) this.visibleStars = this.stars.filter((s) => { const o = (Math.floor(s.y * H) * W + Math.floor(s.x * W)) * 3 return this.hdr[o] === sky[o] && this.hdr[o + 1] === sky[o + 1] && this.hdr[o + 2] === sky[o + 2] @@ -446,7 +449,8 @@ class City extends Canvas { const shade = day ? side * (0.9 + 0.15 * up) : 1 let r = body[0] * shade let g = body[1] * shade - let b = body[2] * shade + // The shaded side takes the blue of the sky. + let b = body[2] * shade * (side < 1 ? 1.25 : 1) const wu = u - 0.004 const wv = v - 0.006 const fu = wu / cw - Math.floor(wu / cw) @@ -457,9 +461,9 @@ class City extends Canvas { const row = Math.floor(wv / (t.windows === 2 ? ch * 1.5 : ch)) const h = hash2(t.seed * 13.1 + col * 1.7, row * 3.3 + k * 17) const glass = day ? 0.55 + 0.2 * up : 0.6 - r = lerp(r * glass, 0.45, day ? 0.25 : 0) - g = lerp(g * glass, 0.6, day ? 0.25 : 0) - b = lerp(b * glass, 0.78, day ? 0.25 : 0) + r = lerp(r * glass, 0.26, day ? 0.35 : 0) + g = lerp(g * glass, 0.46, day ? 0.35 : 0) + b = lerp(b * glass, 0.78, day ? 0.35 : 0) if (h < look.lit * (t.far ? 0.6 : 1)) { const h2 = hash(h * 91.7) const c = palette[h2 < 0.75 ? t.palette % palette.length : Math.floor(h2 * 997) % palette.length] @@ -623,7 +627,7 @@ class City extends Canvas { private paintHighway() { const { A, H } = this const edge = this.paint([0.45, 0.42, 0.38]) - this.polygon(this.rect(-0.01, QUAY, A + 0.01, ROAD), this.paint([0.5, 0.47, 0.42])) + this.polygon(this.rect(-0.01, QUAY, A + 0.01, ROAD), this.paint([0.52, 0.43, 0.34])) this.polygon(this.rect(-0.01, QUAY, A + 0.01, QUAY + 0.004), edge) const rail: Part[] = [cap(-1, (QUAY - 0.012) * H, A * H + 1, (QUAY - 0.012) * H, 0.0012 * H, 0.0012 * H, edge)] for (let x = 0.01; x < A; x += 0.03) rail.push(cap(x * H, QUAY * H, x * H, (QUAY - 0.012) * H, 0.001 * H, 0.001 * H, edge)) @@ -639,7 +643,7 @@ class City extends Canvas { for (let y = UNDER; y < STREET; y += 0.005) this.polygon(this.rect(-0.01, y, A + 0.01, y + 0.0052), this.paint([0.12 + (y - UNDER) * 2, 0.12 + (y - UNDER) * 2, 0.15 + (y - UNDER) * 2])) this.polygon(this.rect(-0.01, STREET, A + 0.01, 0.965), this.paint([0.26, 0.26, 0.28])) for (let x = 0.02; x < A; x += 0.07) this.polygon(this.rect(x, 0.931, x + 0.032, 0.935), this.paint([0.8, 0.72, 0.35])) - this.polygon(this.rect(-0.01, 0.965, A + 0.01, 1.01), this.paint([0.48, 0.46, 0.43])) + this.polygon(this.rect(-0.01, 0.965, A + 0.01, 1.01), this.paint([0.44, 0.37, 0.31])) this.polygon(this.rect(-0.01, 0.965, A + 0.01, 0.969), this.paint([0.62, 0.6, 0.56])) for (let i = 0; this.pillarX(i) - 0.04 < A; i++) { const x = this.pillarX(i) diff --git a/wallpapers/desert.ts b/wallpapers/desert.ts index 570ddf3..328f1c6 100644 --- a/wallpapers/desert.ts +++ b/wallpapers/desert.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { bird, flyAway, startle, type Flier } from "../src/flock" import { TAU, clamp, fbm1, fbm2, hash, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeStorm, paintStorm } from "../src/sky" @@ -66,11 +67,11 @@ const LOOKS: Record = { farMesa: [0.62, 0.36, 0.26], nearMesa: [0.62, 0.27, 0.13], floor: [ - [0.66, 0.52, 0.38], - [0.6, 0.42, 0.26], + [0.66, 0.5, 0.34], + [0.6, 0.4, 0.23], ], - backDune: [0.7, 0.5, 0.3], - frontDune: [0.76, 0.54, 0.3], + backDune: [0.72, 0.48, 0.26], + frontDune: [0.78, 0.52, 0.27], cactus: [0.16, 0.32, 0.12], bone: [0.86, 0.8, 0.68], horn: [0.72, 0.62, 0.45], @@ -283,6 +284,7 @@ class Desert extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.4 this.season = settings.season ?? "summer" this.flowerLight = FLOWER_LIGHT[settings.time] this.dusting = DUSTING[settings.time] @@ -365,6 +367,8 @@ class Desert extends Canvas { this.orbY = look.orb.y * H this.drawSky() this.weather.cover(this.hdr, W, H) + const sky = this.hdr.slice() + const day = look === LOOKS.day const far = mesas(A, 3, 7, [0.06, 0.16], [0.04, 0.09]) const near = mesas(A, 2, 19, [0.05, 0.12], [0.06, 0.13]) this.nearTop = new Float32Array(W) @@ -382,6 +386,8 @@ class Desert extends Canvas { }) this.drawFloor() this.fillBelow(this.nearTop, (x, y, d) => (y > (HORIZON + 0.02) * H ? undefined : this.surface(look.nearMesa, this.nearTop, x, d, 1.6, 0.8, true))) + // By day the mesas and the far floor fade toward the sky, layer by layer. + if (day) haze(this.hdr, sky, W, H, HORIZON, 0.72, 0.12) const backTop = new Float32Array(W) this.frontTop = new Float32Array(W) for (let x = 0; x < W; x++) { @@ -394,6 +400,7 @@ class Desert extends Canvas { const ripple = 0.9 + 0.1 * Math.sin(x * 0.5 + d * 0.9 + fbm1(x * 0.05, 3) * 4) return this.surface(scale(look.frontDune, ripple), this.frontTop, x, d, 2.5, 0.5, false) }) + if (day) mottle(this.hdr, W, H, HORIZON, 1, 0.6, [1.1, 0.98, 0.8], [0.72, 0.7, 0.86]) if (this.season === "spring") this.drawBloom() if (this.activity === "teeming") { for (const fx of [0.47, 0.92]) { diff --git a/wallpapers/farm.ts b/wallpapers/farm.ts index 868c944..e170730 100644 --- a/wallpapers/farm.ts +++ b/wallpapers/farm.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper" @@ -40,7 +41,7 @@ const LOOKS: Record = { clouds: { puffy: true, top: [1.1, 1.1, 1.1], bottom: [0.6, 0.66, 0.78], alpha: 0.9 }, hills: [ [0.42, 0.6, 0.38], - [0.3, 0.55, 0.16], + [0.25, 0.5, 0.12], ], tint: [1, 1, 1], windows: 0, @@ -144,6 +145,14 @@ const CROWN: Record = { } // Where the two trees stand, as fractions of the width. const TREES = [0.42, 0.95] +// Daytime crops on the far fields, as multipliers of the far hill color. +const CROPS: RGB[] = [ + [1, 1, 1], + [0.66, 0.82, 0.62], + [1.1, 1.08, 0.8], + [1.45, 1.12, 0.45], + [1.2, 0.86, 0.55], +] const WOOD: RGB = [0.45, 0.3, 0.16] const BLACK: RGB = [0.05, 0.05, 0.05] @@ -195,6 +204,7 @@ class Farm extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.4 this.season = settings.season ?? "summer" this.hills = GROUND[this.season]?.[settings.time] ?? this.look.hills // Without a weather setting, winter brings a light flurry. @@ -304,17 +314,29 @@ class Farm extends Canvas { farTop[x] = (0.52 - 0.03 * fbm1(u * 1.5, 3) - 0.015 * Math.sin(u * 3)) * H this.nearTop[x] = (0.6 - 0.025 * Math.sin(u * 2.2 + 1) - 0.02 * fbm1(u * 2, 5)) * H } + const day = look === LOOKS.day + const sky = this.hdr.slice() this.fillBelow(farTop, (x, y, d) => { - const field = hash2(Math.floor((x / H) * 5 + (y / H) * 9), Math.floor((y / H) * 22)) + const fu = (x / H) * 5 + (y / H) * 9 + const fv = (y / H) * 22 + const field = hash2(Math.floor(fu), Math.floor(fv)) const k = 1 + (field < 0.33 ? -0.15 : field < 0.66 ? 0 : 0.12) * patchwork const gold = field > golden && season !== "winter" ? 0.3 : 0 - return this.rim([far[0] * k + gold * far[0], far[1] * k + gold * 0.2 * far[1], far[2] * k * (1 - gold)], x, d, 1.5) + const c: RGB = [far[0] * k + gold * far[0], far[1] * k + gold * 0.2 * far[1], far[2] * k * (1 - gold)] + if (!day || season === "winter") return this.rim(c, x, d, 1.5) + // By day each field takes its own crop, from deep green to ploughed ochre, edged by dark hedgerows. + const crop = CROPS[Math.floor(hash2(Math.floor(fu) + 7.1, Math.floor(fv) + 3.3) * CROPS.length)] + const hedge = Math.min(fu - Math.floor(fu), (fv - Math.floor(fv)) * 0.4) < 0.05 ? 0.5 : 1 + return [c[0] * crop[0] * hedge, c[1] * crop[1] * hedge, c[2] * crop[2] * hedge] }) + // The far fields fade into the sky, then the pasture in front of them stays crisp. + if (day) haze(this.hdr, sky, W, H, 0.5, 0.62, 0.24, 0.08) this.fillBelow(this.nearTop, (x, y, d) => { const blades = 1 - grass + grass * Math.sin(x * 1.7 + y * 0.6 + fbm1(x * 0.1, 2) * 6) const depth = 1 - clamp((y / H - 0.6) * 0.6, 0, 0.25) return this.rim([near[0] * blades * depth, near[1] * blades * depth, near[2] * blades * depth], x, d, 2) }) + if (day) mottle(this.hdr, W, H, 0.58, 1, 0.55, season === "winter" ? [1.03, 1, 0.92] : [1.14, 1.06, 0.7], season === "winter" ? [0.62, 0.74, 1] : [0.66, 0.84, 0.66]) // A dirt road winding across the middle distance. for (let x = 0; x < W; x++) { const cy = this.roadY(x / H) * H diff --git a/wallpapers/jungle.ts b/wallpapers/jungle.ts index 850f37c..d12bb1f 100644 --- a/wallpapers/jungle.ts +++ b/wallpapers/jungle.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { TAU, clamp, fbm1, hash, lerp, rand, smoothstep, type RGB } from "../src/math" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -32,11 +33,11 @@ const LOOKS: Record = { style: "front", light: [1, 0.95, 0.75], mist: [ - [0.7, 0.85, 0.72], - [0.32, 0.52, 0.36], - [0.06, 0.12, 0.06], + [0.6, 0.8, 0.6], + [0.22, 0.45, 0.28], + [0.04, 0.1, 0.04], ], - far: [0.33, 0.52, 0.38], + far: [0.3, 0.52, 0.34], water: [0.8, 0.92, 1], shafts: 0.35, tint: [1, 1, 1], @@ -153,6 +154,7 @@ class Jungle extends Canvas { const look = LOOKS[settings.time] const wet = RAINY_MIST[settings.time] this.look = rainy ? { ...look, mist: [mixRGB(look.mist[0], wet, 0.55), mixRGB(look.mist[1], wet, 0.4), look.mist[2]], far: mixRGB(look.far, wet, 0.4), shafts: look.shafts * 0.25 } : look + if (settings.time === "day") this.frame = 0.3 // Without a weather setting, the rainy season brings a light shower. this.weather = new WeatherLayer(settings.weather ?? (rainy ? "rain" : "clear"), settings.time, 0.66, !settings.weather) const count = this.look.night ? 0 : { calm: 0, lively: 2, teeming: 4 }[settings.activity] @@ -239,6 +241,7 @@ class Jungle extends Canvas { const { W, H, A, look } = this this.lighting = look.style === "front" ? { style: "front", dir: DAYLIGHT } : { style: "rim", color: look.light, x: 0.85 * W, y: -0.15 * H } this.drawMist() + const mist = this.hdr.slice() // Distant trees fade into the mist: thin trunks and a hazy band of canopy. for (let i = 0; i < Math.round(A * 7); i++) { const x = (i + 0.2 + hash(i * 3.1) * 0.6) * (A / Math.round(A * 7)) * H @@ -250,6 +253,8 @@ class Jungle extends Canvas { this.lighting, 0.15, ) + // By day the distant trees melt further into the mist. + if (look.style === "front") haze(this.hdr, mist, W, H, 1, 1.01, 0.3) // A rocky cliff with the waterfall pouring over it. const fx = 0.55 * A this.falls = { x: fx * H, top: 0.27 * H, bottom: 0.66 * H, half: (this.rainy ? 0.026 : 0.018) * H } @@ -296,6 +301,8 @@ class Jungle extends Canvas { const c = this.paint(FLOOR) this.blend((y * W + x) * 3, c[0] * k, c[1] * k, c[2] * k, clamp(y + 1 - this.groundPx[x], 0, 1)) } + // Sun flecks and shade across the forest floor. + if (look.style === "front") mottle(this.hdr, W, H, 0.8, 1, 0.3, [1.5, 1.35, 0.8], [0.7, 0.8, 0.8]) for (let i = 0; i < Math.round(A * 6); i++) { const x = (hash(i * 3.9) * A) * H this.drawFern(x, this.groundPx[clamp(Math.round(x), 0, W - 1)] + 2, (0.08 + hash(i * 1.1) * 0.06) * H, hash(i * 5.5) > 0.5 ? 1 : -1) diff --git a/wallpapers/ocean.ts b/wallpapers/ocean.ts index ec5e136..6995f74 100644 --- a/wallpapers/ocean.ts +++ b/wallpapers/ocean.ts @@ -119,6 +119,8 @@ class Ocean extends Canvas { private floorPx = new Float32Array(0) private farPx = new Float32Array(0) private midPx = new Float32Array(0) + // By day, broad lighter and darker stretches along the near ridge, so it reads as rock rather than a flat cutout. + private midTone = new Float32Array(0) private pad = 0 private lastA = 1 private fish: Fish[] = [] @@ -378,12 +380,14 @@ class Ocean extends Canvas { this.pad = Math.ceil(H * 0.2) this.farPx = new Float32Array(W + this.pad * 2) this.midPx = new Float32Array(W + this.pad * 2) + this.midTone = new Float32Array(W + this.pad * 2) for (let x = 0; x < W + this.pad * 2; x++) { const u = (x - this.pad) / H const spires = Math.pow(noise1(u * 4.2, 9), 5) * 0.32 this.farPx[x] = (0.56 - (fbm1(u * 1.4, 7) - 0.5) * 0.3 - spires) * H const arches = Math.pow(noise1(u * 6.5, 12), 7) * 0.25 this.midPx[x] = (0.72 - (fbm1(u * 2.3, 11) - 0.5) * 0.2 - arches) * H + this.midTone[x] = this.timeOfDay === "day" ? 0.6 + 0.55 * smoothstep(0.3, 0.7, fbm1(u * 3.1, 17)) : 1 } // Three kelp strands in the gaps between the creatures' areas. this.kelp = this.activity === "teeming" ? [[0.05, 0.28], [0.5, 0.21], [0.96, 0.3]].map(([x, height]) => ({ x: x * A, height, phase: x * 9 })) : [] @@ -612,7 +616,7 @@ class Ocean extends Canvas { // Depth-graded water with god rays, distant ridges, and a bright rippling surface with caustics. private drawWater() { - const { W, H, hdr, rowCol, rayFade, farPx, midPx } = this + const { W, H, hdr, rowCol, rayFade, farPx, midPx, midTone } = this const t = this.time const surf = SURFACE * H const camFar = Math.round(Math.sin(t * 0.09) * H * 0.04) + this.pad @@ -652,9 +656,10 @@ class Ocean extends Canvas { const near = midPx[x + camMid] if (y > near) { const k = smoothstep(near, near + 2, y) - r = lerp(r, r * 0.35, k) - g = lerp(g, g * 0.42, k) - b = lerp(b, b * 0.5, k) + const tone = midTone[x + camMid] + r = lerp(r, r * 0.35 * tone, k) + g = lerp(g, g * 0.42 * tone, k) + b = lerp(b, b * 0.5 * tone, k) } const ray = RAYS_A[((x + ry1) | 0) & 1023] * (0.45 + 0.55 * RAYS_B[((x * 0.7 + ry2) | 0) & 1023]) * rf * flick hdr[i] = r + ray * 0.5 diff --git a/wallpapers/prehistoric.ts b/wallpapers/prehistoric.ts index d37610a..2f749f1 100644 --- a/wallpapers/prehistoric.ts +++ b/wallpapers/prehistoric.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { bird, flyAway, startle, type Flier } from "../src/flock" import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" @@ -58,7 +59,7 @@ const LOOKS: Record = { forest: [0.14, 0.3, 0.13], ground: [ [0.36, 0.5, 0.2], - [0.24, 0.42, 0.12], + [0.2, 0.38, 0.1], ], rock: [0.32, 0.25, 0.22], lava: [ @@ -192,6 +193,7 @@ class Prehistoric extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.4 this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, HORIZON) this.allStars = makeStars(this.look.stars, 0.5) this.clouds = makeClouds(this.look.clouds.puffy ? 4 : 5, this.look.clouds.puffy, 0.06, 0.3) @@ -277,6 +279,8 @@ 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 sky = this.hdr.slice() + const day = look === LOOKS.day const vx = VENT_X * A const ridgeTop = new Float32Array(W) const volcanoTop = new Float32Array(W) @@ -311,6 +315,8 @@ class Prehistoric extends Canvas { const b = lerp(look.rock[2] * (1 + ash), look.forest[2], green) * k return this.rim([r, gg, b], x, d, 2) }) + // By day the ridge and volcano fade into the sky, then the forest and far plain, while the foreground stays crisp. + if (day) haze(this.hdr, sky, W, H, 0.62, 0.63, 0.1) this.paintLava(vx) this.fillBelow(forestTop, (x, y, d) => { const k = 0.8 + 0.2 * fbm1(x * 0.3, 41) + (hash2(x, y) - 0.5) * 0.1 @@ -322,6 +328,10 @@ class Prehistoric extends Canvas { const blades = 0.9 + 0.1 * Math.sin(x * 1.7 + y * 0.6 + fbm1(x * 0.1, 2) * 6) + (hash2(x, y) - 0.5) * 0.05 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 (day) { + mottle(this.hdr, W, H, 0.62, 1, 0.55, [1.16, 1.04, 0.66], [0.68, 0.84, 0.72]) + haze(this.hdr, sky, W, H, 0.62, 0.8, 0.14, 0, 0.56) + } if (this.activity === "teeming") this.paintLake() this.trees = [0.08 * A, Math.max(0.4 * A, 0.08 * A + 0.45)] this.drawTree(this.trees[0], 0.665, 0.29) diff --git a/wallpapers/space.ts b/wallpapers/space.ts index 6c39a47..3da0737 100644 --- a/wallpapers/space.ts +++ b/wallpapers/space.ts @@ -49,8 +49,8 @@ const LOOKS: Record = { ring: 1, lighting: "front", tint: [1, 1, 1], - ringColor: [0.75, 0.68, 0.55], - rockColor: [0.75, 0.68, 0.6], + ringColor: [0.78, 0.64, 0.46], + rockColor: [0.62, 0.54, 0.46], starTints: false, }, sunset: { @@ -139,6 +139,7 @@ class Space extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.25 if (settings.activity === "teeming") this.rocks = Array.from({ length: 6 }, (_, i) => ({ x: Math.random() * 2, @@ -287,7 +288,11 @@ class Space extends Canvas { const a = 0.75 * density const k = look.ring * (0.6 + 0.4 * density) const o = y * W + x - const [rr, rg, rb] = look.ringColor + // By day the bands alternate between warm tan and cool gray dust. + const warm = look.lighting === "front" ? 0.16 * Math.sin(rho * 9 + 1) : 0 + const rr = look.ringColor[0] * (1 + warm) + const rg = look.ringColor[1] + const rb = look.ringColor[2] * (1 - warm * 1.5) if (qy < 0) { this.blend(o * 3, rr * k, rg * k, rb * k, a) if (a > 0.3) this.ringMask[o] = 1 diff --git a/wallpapers/tundra.ts b/wallpapers/tundra.ts index f7d1dd3..b3668d6 100644 --- a/wallpapers/tundra.ts +++ b/wallpapers/tundra.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { TAU, clamp, fbm1, hash, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" @@ -39,18 +40,18 @@ const LOOKS: Record = { style: "front", light: [1, 0.97, 0.9], sky: [ - [0, [0.2, 0.42, 0.75]], - [0.3, [0.4, 0.62, 0.85]], - [HORIZON, [0.75, 0.85, 0.92]], + [0, [0.12, 0.32, 0.7]], + [0.3, [0.28, 0.52, 0.84]], + [HORIZON, [0.62, 0.76, 0.9]], ], orb: { x: 0.78, y: 0.22, r: 0.032, core: [4.5, 4.3, 3.8], glow: [1, 0.95, 0.85], near: 0.4, wide: 0.12 }, stars: 0, clouds: { puffy: true, top: [1.05, 1.08, 1.12], bottom: [0.62, 0.7, 0.82], alpha: 0.85 }, - snow: [0.8, 0.87, 0.95], - shade: [0.55, 0.66, 0.85], - rock: [0.35, 0.4, 0.5], - peaks: [0.85, 0.9, 0.97], - ice: [0.55, 0.75, 0.88], + snow: [0.76, 0.72, 0.64], + shade: [0.2, 0.36, 0.76], + rock: [0.26, 0.29, 0.4], + peaks: [0.92, 0.86, 0.78], + ice: [0.4, 0.66, 0.86], flakes: [0.9, 0.94, 1], tint: [1, 1, 1], aurora: 0, @@ -152,6 +153,7 @@ class Tundra extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.4 this.stars = makeStars(this.look.stars, 0.45) this.clouds = makeClouds(this.look.clouds.puffy ? 3 : 4, this.look.clouds.puffy, 0.06, 0.28) // Its own gentle snowfall under a clear sky, unless weather is chosen. @@ -247,6 +249,7 @@ class Tundra 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 sky = this.hdr.slice() this.drawMountains() // Gentle drifts near the horizon, then the snowfield sweeping to the bottom of the screen. const drift = new Float32Array(W) @@ -255,11 +258,17 @@ class Tundra extends Canvas { for (let y = Math.max(0, Math.floor(drift[x])); y < H; y++) { const v = y / H const waves = 0.5 + 0.5 * Math.sin((x / H) * 6 + v * 40 + fbm1(x * 0.02, 3) * 4) - const k = smoothstep(0.3, 1, waves) * 0.35 * (1 - (v - 0.6) * 0.8) - const c = lerpRGB(look.snow, look.shade, k) + const k = smoothstep(0.3, 1, waves) * (look === LOOKS.day ? 0.2 : 0.35) * (1 - (v - 0.6) * 0.8) + // By day the foreground lies in blue shadow, framing the sunlit middle distance. + const c = lerpRGB(look.snow, look.shade, look === LOOKS.day ? Math.min(1, k + smoothstep(0.7, 1.1, v) * 0.45) : k) 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)) } + // By day, broad blue cloud shadows drift over the snow, and the far field and peaks fade into the sky. + if (look === LOOKS.day) { + mottle(this.hdr, W, H, 0.6, 1, 0.7, [1.04, 1, 0.9], [0.5, 0.66, 1]) + haze(this.hdr, sky, W, H, 0.6, 0.8, 0.15, 0) + } 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) @@ -292,7 +301,8 @@ class Tundra extends Canvas { for (let y = Math.max(0, Math.floor(top[x])); y < (HORIZON + 0.06) * H; y++) { const haze = clamp((y / H - 0.35) * 1.2, 0, 0.4) let c = y < snowLine ? look.peaks : look.rock - if (look.style === "front") c = scaleRGB(c, slope > 0.3 ? 1.05 : slope < -0.3 ? 0.72 : 0.9) + // Sunlit faces warm, shaded ones a crisp blue. + if (look.style === "front") c = mulRGB(c, slope > 0.3 ? [1.05, 1, 0.92] : slope < -0.3 ? [0.45, 0.6, 0.92] : [0.8, 0.82, 0.9]) const edge = look.style === "rim" ? Math.exp(-(y + 0.5 - top[x]) / 1.8) * 0.5 : 0 c = lerpRGB(c, look.sky[look.sky.length - 1][1], haze * 0.5) 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 - top[x], 0, 1)) @@ -659,6 +669,10 @@ function scaleRGB(c: RGB, k: number): RGB { return [c[0] * k, c[1] * k, c[2] * k] } +function mulRGB(a: RGB, b: RGB): RGB { + return [a[0] * b[0], a[1] * b[1], a[2] * b[2]] +} + export const tundra: Wallpaper = { id: "tundra", name: "Tundra", diff --git a/wallpapers/zen.ts b/wallpapers/zen.ts index c1f2340..0ed21f6 100644 --- a/wallpapers/zen.ts +++ b/wallpapers/zen.ts @@ -1,5 +1,6 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" +import { haze, mottle } from "../src/grade" import { bird, flyAway, startle, type Flier } from "../src/flock" import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math" import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" @@ -59,7 +60,7 @@ const LOOKS: Record = { snow: [1, 1.02, 1.08], mist: [0.72, 0.82, 0.9], moss: [0.2, 0.4, 0.12], - gravel: [0.8, 0.76, 0.66], + gravel: [0.72, 0.66, 0.55], water: [0.03, 0.14, 0.13], mirror: 0.55, glitter: 0.15, @@ -234,6 +235,7 @@ class Zen extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + if (settings.time === "day") this.frame = 0.4 this.season = settings.season ?? "spring" this.weather = new WeatherLayer(settings.weather ?? (this.season === "winter" ? "snow" : "clear"), settings.time, HORIZON, !settings.weather) this.stars = makeStars(this.look.stars, 0.45) @@ -343,9 +345,13 @@ class Zen extends Canvas { this.cherry = { x: A - 0.2, y: 0.27 } paintSky(this.hdr, W, H, look.sky, look.orb) this.weather.cover(this.hdr, W, H) + const clear = this.hdr.slice() this.drawHills() + if (look === LOOKS.day) haze(this.hdr, clear, W, H, HORIZON, GROUND, 0.15) const sky = this.hdr.slice() this.drawGround() + // By day, broad warm and cool patches of sun and shade lie over the raked gravel. + if (look === LOOKS.day) mottle(this.hdr, W, H, GROUND, 1, 0.5, [1.08, 1, 0.84], [0.76, 0.84, 0.96]) this.drawBackGarden() // Find the water, note what lies under its edges, and work out its still color with the sky mirrored in it. const p = this.pond