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
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,11 @@ Give night clear light structure so things stay readable: a lighter band at the
surface) for silhouettes to stand against, a strong rim `light` from the moon's side, and a few local pools of warm or
glowing light. `lightPool()` in `src/light.ts` lights an ellipse in the light's own color, so ground and creatures
passing through it pick it up; draw it each frame after the creatures it should light. `campfire()` and
`fireflyLight()` there are ready-made pools. Keep the area behind text no brighter or busier than before.
`fireflyLight()` there are ready-made pools. For light that moves or flickers over a wide area (aurora on snow, a
fire, a buzzing sign), give the scene a `LightMap` from `src/lightmap.ts`: `clear()` it each frame, write `pool()`s or
single `node()`s into its coarse grid, then `apply()` it once after the things it should light. It is cheap over large
areas and dims itself behind the middle of the screen; `firelight()` there gives a slow flicker. Keep the area behind
text no brighter or busier than before, and keep flicker slow.

## Seasons and weather

Expand Down
Binary file modified screenshots/beach-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/city-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/city-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/farm-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/prehistoric-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/zen-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { eggWait } from "./egg"
export { bird, flyAway, startle, type Flier } from "./flock"
export { haze, mottle } from "./grade"
export { campfire, fireflyLight, lightPool } from "./light"
export { LightMap, firelight } from "./lightmap"
export { TAU, clamp, fbm1, fbm2, hash, hash2, hashString, hsv, lerp, noise1, noise2, rand, smoothstep, type RGB } from "./math"
export { fillShadow, groundShadow, sunShade, type Shade } from "./shadow"
export { STAR_TINTS, driftClouds, makeClouds, makeShadows, makeStars, makeStorm, paintClouds, paintHaze, paintShadows, paintSky, paintStars, paintStorm, type Cloud, type CloudKind, type Orb, type Star } from "./sky"
Expand Down
135 changes: 135 additions & 0 deletions src/lightmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { noise1, smoothstep, type RGB } from "./math"
import { touch } from "./touched"

// A coarse grid of light, one node every H/30 pixels, that moving and flickering lights write into each frame;
// apply() then spreads it over the scene in one pass, bilinearly between nodes. Like lightPool(), lift lights what is
// already there in proportion to its brightest channel, so lit ground keeps its texture, and glow adds a faint haze.
// Columns behind the middle of the screen, where text usually sits, get less of it.
export class LightMap {
cell = 1
cols = 0
rows = 0
private W = 0
private H = 0
private lift = new Float32Array(0)
private glow = new Float32Array(0)
private lit = new Uint8Array(0)
private dim = new Float32Array(0)
// Bounding box of the nodes written this frame.
private box = [0, 0, -1, -1]

// Empties the map, first resizing it if the canvas changed size.
clear(W: number, H: number) {
if (W !== this.W || H !== this.H) {
this.W = W
this.H = H
this.cell = Math.max(4, Math.round(H / 30))
this.cols = Math.floor((W - 0.5) / this.cell) + 2
this.rows = Math.floor((H - 0.5) / this.cell) + 2
this.lift = new Float32Array(this.cols * this.rows * 3)
this.glow = new Float32Array(this.cols * this.rows * 3)
this.lit = new Uint8Array(this.cols * this.rows)
this.dim = Float32Array.from({ length: this.cols }, (_, i) => 1 - 0.45 * smoothstep(0.4, 0.15, Math.abs((i * this.cell) / W - 0.5)))
this.box = [0, 0, -1, -1]
}
const [i0, j0, i1, j1] = this.box
for (let j = j0; j <= j1; j++) {
this.lift.fill(0, (j * this.cols + i0) * 3, (j * this.cols + i1 + 1) * 3)
this.glow.fill(0, (j * this.cols + i0) * 3, (j * this.cols + i1 + 1) * 3)
this.lit.fill(0, j * this.cols + i0, j * this.cols + i1 + 1)
}
this.box = [this.cols, this.rows, -1, -1]
}

// Adds light at node (i, j): color times lift and times glow.
node(i: number, j: number, color: RGB, lift: number, glow: number) {
const n = j * this.cols + i
const o = n * 3
this.lift[o] += color[0] * lift
this.lift[o + 1] += color[1] * lift
this.lift[o + 2] += color[2] * lift
this.glow[o] += color[0] * glow
this.glow[o + 1] += color[1] * glow
this.glow[o + 2] += color[2] * glow
this.lit[n] = 1
const b = this.box
if (i < b[0]) b[0] = i
if (j < b[1]) b[1] = j
if (i > b[2]) b[2] = i
if (j > b[3]) b[3] = j
}

// A pool of light falling off over an ellipse (pixels) around (cx, cy), as lightPool() draws.
pool(cx: number, cy: number, rx: number, ry: number, color: RGB, lift: number, glow: number) {
const c = this.cell
for (let j = Math.max(0, Math.ceil((cy - ry) / c)); j <= Math.min(this.rows - 1, Math.floor((cy + ry) / c)); j++) {
const dy = (j * c - cy) / ry
for (let i = Math.max(0, Math.ceil((cx - rx) / c)); i <= Math.min(this.cols - 1, Math.floor((cx + rx) / c)); i++) {
const dx = (i * c - cx) / rx
const d = 1 - dx * dx - dy * dy
if (d > 0) this.node(i, j, color, lift * d * d, glow * d * d)
}
}
}

apply(hdr: Float32Array) {
const [i0, j0, i1, j1] = this.box
if (i1 < i0) return
applyLight(hdr, this.W, this.H, this.cell, this.cols, this.lift, this.glow, this.lit, this.dim, Math.max(0, i0 - 1), Math.max(0, j0 - 1), Math.min(this.cols - 2, i1), Math.min(this.rows - 2, j1))
}
}

// Firelight: a slow, uneven flicker between about 0.8 and 1, different for each seed. t is scene time.
export function firelight(t: number, seed: number) {
return 0.8 + 0.13 * noise1(t * 1.1, seed) + 0.07 * noise1(t * 2.4, seed + 3)
}

// Spreads the map over hdr block by block (a block lies between four nodes), skipping blocks with no light at their
// corners: each pixel gains its capped brightness times the interpolated lift, plus the interpolated glow. Each row's
// runs of lit blocks are touched (see Canvas.touched).
function applyLight(hdr: Float32Array, W: number, H: number, cell: number, cols: number, lift: Float32Array, glow: Float32Array, lit: Uint8Array, dim: Float32Array, bi0: number, bj0: number, bi1: number, bj1: number) {
for (let bj = bj0; bj <= bj1; bj++) {
let run = -1
for (let bi = bi0; bi <= bi1 + 1; bi++) {
const a = bj * cols + bi
const c = a + cols
const on = bi <= bi1 && (lit[a] || lit[a + 1] || lit[c] || lit[c + 1])
if (!on && run >= 0) touch(hdr, run * cell, bj * cell, bi * cell - 1, (bj + 1) * cell - 1)
if (!on) {
run = -1
continue
}
if (run < 0) run = bi
const dl = dim[bi]
const dr = dim[bi + 1]
const x0 = bi * cell
const x1 = Math.min(W, x0 + cell)
for (let y = bj * cell; y < Math.min(H, (bj + 1) * cell); y++) {
const fy = (y + 0.5) / cell - bj
const gy = 1 - fy
// The block's left and right edges at this row.
const lr = (lift[a * 3] * gy + lift[c * 3] * fy) * dl
const lg = (lift[a * 3 + 1] * gy + lift[c * 3 + 1] * fy) * dl
const lb = (lift[a * 3 + 2] * gy + lift[c * 3 + 2] * fy) * dl
const rr = (lift[a * 3 + 3] * gy + lift[c * 3 + 3] * fy) * dr
const rg = (lift[a * 3 + 4] * gy + lift[c * 3 + 4] * fy) * dr
const rb = (lift[a * 3 + 5] * gy + lift[c * 3 + 5] * fy) * dr
const gl = (glow[a * 3] * gy + glow[c * 3] * fy) * dl
const gg = (glow[a * 3 + 1] * gy + glow[c * 3 + 1] * fy) * dl
const gb = (glow[a * 3 + 2] * gy + glow[c * 3 + 2] * fy) * dl
const hr = (glow[a * 3 + 3] * gy + glow[c * 3 + 3] * fy) * dr
const hg = (glow[a * 3 + 4] * gy + glow[c * 3 + 4] * fy) * dr
const hb = (glow[a * 3 + 5] * gy + glow[c * 3 + 5] * fy) * dr
for (let x = x0; x < x1; x++) {
const fx = (x + 0.5) / cell - bi
const o = (y * W + x) * 3
// Capped, so things already glowing don't wash out to white.
const m = Math.min(0.2, Math.max(hdr[o], hdr[o + 1], hdr[o + 2]))
hdr[o] += m * (lr + (rr - lr) * fx) + gl + (hr - gl) * fx
hdr[o + 1] += m * (lg + (rg - lg) * fx) + gg + (hg - gg) * fx
hdr[o + 2] += m * (lb + (rb - lb) * fx) + gb + (hb - gb) * fx
}
}
}
}
}
7 changes: 6 additions & 1 deletion wallpapers/beach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { eggWait } from "../src/egg"
import { haze, mottle } from "../src/grade"
import { bird, flyAway, startle, type Flier } from "../src/flock"
import { campfire, lightPool } from "../src/light"
import { LightMap } from "../src/lightmap"
import { TAU, clamp, fbm1, hash, hash2, lerp, noise1, rand, smoothstep, type RGB } from "../src/math"
import { groundShadow, sunShade, type Shade } from "../src/shadow"
import { driftClouds, makeClouds, makeShadows, makeStars, makeStorm, paintClouds, paintShadows, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky"
Expand Down Expand Up @@ -205,6 +206,7 @@ class Beach extends Canvas {
private bottle = { wait: eggWait() * TIME_SCALE, t: -1 }
private readonly season: Season
private readonly weather: WeatherLayer
private lights = new LightMap()
private swimmers: { x: number; y: number; phase: number }[] = []

constructor(settings: Settings) {
Expand Down Expand Up @@ -484,7 +486,10 @@ class Beach extends Canvas {
const x = 0.17 * this.A * H
const gy = 0.92 * H
const fire = campfire(x, gy, H * 1.2, this.time)
lightPool(this.hdr, this.W, H, x, gy - 0.03 * H, 0.36 * H, 0.13 * H, [1, 0.4, 0.07], 2.6 * fire.flicker, 0.04 * fire.flicker)
// The light leans and flickers with the flames.
this.lights.clear(this.W, H)
this.lights.pool(x + (noise1(this.time * 0.9, 5) - 0.5) * 0.05 * H, gy - 0.03 * H, 0.36 * H, 0.13 * H, [1, 0.4, 0.07], 2.6 * fire.flicker, 0.04 * fire.flicker)
this.lights.apply(this.hdr)
this.shape(fire.logs, { style: "rim", color: [1.4, 0.5, 0.1], x, y: gy - 0.03 * H })
for (const flame of fire.flames) this.shape(flame, { style: "front", dir: [0, 0, 1] }, 0)
}
Expand Down
41 changes: 34 additions & 7 deletions wallpapers/city.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { eggWait } from "../src/egg"
import { bird, flyAway, startle, type Flier } from "../src/flock"
import { haze } from "../src/grade"
import { lightPool } from "../src/light"
import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math"
import { LightMap } from "../src/lightmap"
import { TAU, clamp, fbm1, hash, hash2, lerp, noise1, rand, smoothstep, type RGB } from "../src/math"
import { groundShadow, sunShade, type Shade } from "../src/shadow"
import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintHaze, 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 @@ -260,6 +261,7 @@ class City extends Canvas {
private rain: { x: number; y: number; speed: number }[] = []
// Rain or snow from the weather setting; the city's own night rain only falls without one.
private readonly weather: WeatherLayer
private lights = new LightMap()
private readonly wet: boolean
private storm = makeStorm()
private pigeons: Flier[] = []
Expand Down Expand Up @@ -318,6 +320,7 @@ class City extends Canvas {
render() {
const { W, H, look } = this
this.restore()
this.lights.clear(W, H)
if (!this.weather.covered) paintStars(this.hdr, W, H, this.visibleStars, this.time, BANK, 0.4, this.points)
const [top, bottom] = this.weather.scud ?? [look.clouds.top, look.clouds.bottom]
paintClouds(this.hdr, W, H, this.clouds, top, bottom, look.clouds.alpha, this.weather.covered ? undefined : look.orb)
Expand All @@ -339,6 +342,8 @@ class City extends Canvas {
for (const w of this.walkers) this.drawWalker(w)
// At night the lamps under the highway light the sidewalk and the people passing beneath them.
if (look.night) for (let i = 0; this.pillarX(i) - 0.21 < this.A + 0.1; i++) lightPool(this.hdr, W, H, (this.pillarX(i) - 0.21) * H, 0.96 * H, 0.14 * H, 0.08 * H, [1, 0.5, 0.12], 3, 0)
if (this.activity === "teeming" && look.glow > 0) this.buzzNeon()
this.lights.apply(this.hdr)
for (const b of this.pigeons) this.shape(bird(b, H, 0.022 * H, this.paint([0.42, 0.44, 0.5])), this.lighting, 0.4)
this.drawRain()
this.weather.draw(this.hdr, W, H)
Expand Down Expand Up @@ -398,11 +403,7 @@ class City extends Canvas {
this.river = this.hdr.slice()
this.paintBridge()
this.paintHighway()
if (this.activity === "teeming") {
const px = this.pillarX(1)
this.neon({ x0: px - 0.012, y0: UNDER + 0.006, x1: px + 0.012, y1: STREET - 0.012, color: NEON[0], seed: 7 }, true, true)
this.neon({ x0: this.pillarX(3) - 0.012, y0: UNDER + 0.01, x1: this.pillarX(3) + 0.012, y1: STREET - 0.015, color: NEON[2], seed: 11 }, true, true)
}
if (this.activity === "teeming") for (const s of this.streetSigns()) this.neon(s, true, true)
// Water is wherever nothing solid has been painted over the river.
this.water = new Uint8Array(W * H)
for (let i = Math.floor(BANK * H) * W; i < Math.min(H, Math.ceil(QUAY * H)) * W; i++) this.water[i] = this.hdr[i * 3] === this.river[i * 3] && this.hdr[i * 3 + 2] === this.river[i * 3 + 2] ? 1 : 0
Expand Down Expand Up @@ -560,6 +561,28 @@ class City extends Canvas {
}
}

// The two neon signs hung under the highway.
private streetSigns(): Sign[] {
const a = this.pillarX(1)
const b = this.pillarX(3)
return [
{ x0: a - 0.012, y0: UNDER + 0.006, x1: a + 0.012, y1: STREET - 0.012, color: NEON[0], seed: 7 },
{ x0: b - 0.012, y0: UNDER + 0.01, x1: b + 0.012, y1: STREET - 0.015, color: NEON[2], seed: 11 },
]
}

// The signs under the highway throw their color on the street below, and now and then one buzzes and dims for a
// moment, its light on the street dimming with it.
private buzzNeon() {
const { H, look } = this
for (const s of this.streetSigns()) {
const dip = smoothstep(0.78, 0.95, noise1(this.time * 1.5, s.seed))
const k = look.glow * (1 - 0.6 * dip)
this.lights.pool(((s.x0 + s.x1) / 2) * H, (STREET + 0.035) * H, 0.08 * H, 0.055 * H, s.color, 2.2 * k, 0.012 * k)
if (dip > 0.01) this.polygon(this.rect(s.x0, s.y0, s.x1, s.y1), this.paint([0.1, 0.08, 0.12]), 0.55 * dip)
}
}

// Neon signs on a few towers: a tall one down a facade and one on a rooftop.
private placeSigns(near: Tower[]) {
const tall = near.filter((t, i) => i % 3 === 1 && BANK - t.tiers[0].top > 0.16)
Expand Down Expand Up @@ -788,7 +811,11 @@ class City extends Canvas {
const on = hash(Math.floor(this.time / 12 + f.phase)) > 0.45
this.polygon(this.rect(f.x, f.y, f.x + f.w, f.y + f.h), on ? f.on : f.off)
}
if (this.sign && look.glow > 0 && hash(Math.floor(this.time * 0.4) + 3.7) > 0.15) this.neon(this.sign, true, true)
if (!this.sign || look.glow <= 0 || hash(Math.floor(this.time * 0.4) + 3.7) <= 0.15) return
const s = this.sign
this.neon(s, true, true)
// While it is lit, it tints the facade and windows around it.
this.lights.pool(((s.x0 + s.x1) / 2) * H, ((s.y0 + s.y1) / 2) * H, 0.04 * H, 0.07 * H, s.color, 1.5 * look.glow, 0.02 * look.glow)
}

// Trees along the promenade between the highway lamps, nodding in the wind, and two flags that hang slack in calm
Expand Down
11 changes: 8 additions & 3 deletions wallpapers/desert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { chain } from "../src/creature"
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 { TAU, clamp, fbm1, fbm2, hash, lerp, noise1, rand, smoothstep, type RGB } from "../src/math"
import { groundShadow, sunShade, type Shade } from "../src/shadow"
import { campfire, lightPool } from "../src/light"
import { campfire } from "../src/light"
import { LightMap } from "../src/lightmap"
import { addOrb, onOrb } from "../src/orbs"
import { driftClouds, makeClouds, makeStorm, paintClouds, paintStorm, type Cloud } from "../src/sky"
import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper"
Expand Down Expand Up @@ -281,6 +282,7 @@ class Desert extends Canvas {
private readonly dusting: RGB
private readonly weather: WeatherLayer
private readonly shade: Shade
private lights = new LightMap()

constructor(settings: Settings) {
super()
Expand Down Expand Up @@ -660,7 +662,10 @@ class Desert extends Canvas {
const x = 0.21 * this.A * H
const gy = this.ground(0.21) + 0.01 * H
const fire = campfire(x, gy, H, this.time)
lightPool(this.hdr, this.W, H, x, gy - 0.03 * H, 0.32 * H, 0.14 * H, [1, 0.4, 0.07], 2.6 * fire.flicker, 0.04 * fire.flicker)
// The light leans and flickers with the flames.
this.lights.clear(this.W, H)
this.lights.pool(x + (noise1(this.time * 0.9, 5) - 0.5) * 0.05 * H, gy - 0.03 * H, 0.32 * H, 0.14 * H, [1, 0.4, 0.07], 2.6 * fire.flicker, 0.04 * fire.flicker)
this.lights.apply(this.hdr)
this.shape(fire.logs, { style: "rim", color: [1.4, 0.5, 0.1], x, y: gy - 0.03 * H })
for (const flame of fire.flames) this.shape(flame, { style: "front", dir: [0, 0, 1] }, 0)
}
Expand Down
13 changes: 10 additions & 3 deletions wallpapers/farm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas"
import { bob, body, chain, gait, limb, quadruped, stepGait, stride, type Gait } from "../src/creature"
import { eggWait } from "../src/egg"
import { haze, mottle } from "../src/grade"
import { fireflyLight, lightPool } from "../src/light"
import { fireflyLight } from "../src/light"
import { LightMap, firelight } from "../src/lightmap"
import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math"
import { groundShadow, sunShade, type Shade } from "../src/shadow"
import { driftClouds, makeClouds, makeShadows, makeStars, makeStorm, paintClouds, paintShadows, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky"
Expand Down Expand Up @@ -202,6 +203,7 @@ class Farm extends Canvas {
private egg: { wait: number; t: number; cow: Walker | undefined } = { wait: eggWait() * TIME_SCALE, t: -1, cow: undefined }
private readonly season: Season
private readonly weather: WeatherLayer
private lights = new LightMap()
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.
Expand Down Expand Up @@ -317,8 +319,13 @@ class Farm extends Canvas {
for (const { w, kind } of herd) kind === "cow" ? this.drawCow(w) : this.drawPig(w)
this.drawEgg()
if (this.activity === "teeming") this.drawFarmer()
// At night the open barn door spills warm light across the yard and whatever is in it.
if (this.look.windows === 1) lightPool(this.hdr, this.W, this.H, this.barnX * this.H, 0.67 * this.H, 0.24 * this.H, 0.07 * this.H, [1, 0.55, 0.15], 3, 0.02)
// At night the open barn door spills a lantern's flickering light across the yard and whatever is in it.
this.lights.clear(this.W, this.H)
if (this.look.windows === 1) {
const k = firelight(this.time * 0.8, 9)
this.lights.pool(this.barnX * this.H, 0.67 * this.H, 0.24 * this.H, 0.07 * this.H, [1, 0.55, 0.15], 3.3 * k, 0.022 * k)
}
this.lights.apply(this.hdr)
this.drawCornRow(3)
this.drawFireflies()
this.drawLeaves()
Expand Down
Loading
Loading