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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions src/canvas.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions src/grade.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
25 changes: 18 additions & 7 deletions wallpapers/beach.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -44,12 +45,12 @@ const LOOKS: Record<Time, Look> = {
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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]))
Expand All @@ -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])
Expand Down Expand Up @@ -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++) {
Expand All @@ -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
Expand Down
34 changes: 19 additions & 15 deletions wallpapers/city.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -55,10 +56,10 @@ const LOOKS: Record<Time, Look> = {
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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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]
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down
15 changes: 11 additions & 4 deletions wallpapers/desert.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -66,11 +67,11 @@ const LOOKS: Record<Time, Look> = {
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],
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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++) {
Expand All @@ -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]) {
Expand Down
Loading
Loading