diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30a8407..a781ca6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/screenshots/beach-night.png b/screenshots/beach-night.png index 684ba4f..e73b4d6 100644 Binary files a/screenshots/beach-night.png and b/screenshots/beach-night.png differ diff --git a/screenshots/city-night.png b/screenshots/city-night.png index 960a387..4e3bc69 100644 Binary files a/screenshots/city-night.png and b/screenshots/city-night.png differ diff --git a/screenshots/city-sunset.png b/screenshots/city-sunset.png index 119c471..87bdf1c 100644 Binary files a/screenshots/city-sunset.png and b/screenshots/city-sunset.png differ diff --git a/screenshots/desert-night.png b/screenshots/desert-night.png index 65d933c..08ab158 100644 Binary files a/screenshots/desert-night.png and b/screenshots/desert-night.png differ diff --git a/screenshots/farm-night.png b/screenshots/farm-night.png index 4c9f528..42faa55 100644 Binary files a/screenshots/farm-night.png and b/screenshots/farm-night.png differ diff --git a/screenshots/prehistoric-night.png b/screenshots/prehistoric-night.png index 4cbd1df..750ff48 100644 Binary files a/screenshots/prehistoric-night.png and b/screenshots/prehistoric-night.png differ diff --git a/screenshots/tundra-night.png b/screenshots/tundra-night.png index 3296fe3..e50fb3d 100644 Binary files a/screenshots/tundra-night.png and b/screenshots/tundra-night.png differ diff --git a/screenshots/tundra-sunset.png b/screenshots/tundra-sunset.png index fe21dd4..2f50393 100644 Binary files a/screenshots/tundra-sunset.png and b/screenshots/tundra-sunset.png differ diff --git a/screenshots/zen-night.png b/screenshots/zen-night.png index ac1cd11..e950ef8 100644 Binary files a/screenshots/zen-night.png and b/screenshots/zen-night.png differ diff --git a/src/api.ts b/src/api.ts index c02de66..7b995ad 100644 --- a/src/api.ts +++ b/src/api.ts @@ -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" diff --git a/src/lightmap.ts b/src/lightmap.ts new file mode 100644 index 0000000..dc6b650 --- /dev/null +++ b/src/lightmap.ts @@ -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 + } + } + } + } +} diff --git a/wallpapers/beach.ts b/wallpapers/beach.ts index 1f9f52d..2aac1cf 100644 --- a/wallpapers/beach.ts +++ b/wallpapers/beach.ts @@ -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" @@ -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) { @@ -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) } diff --git a/wallpapers/city.ts b/wallpapers/city.ts index d703bb9..6fbd8ea 100644 --- a/wallpapers/city.ts +++ b/wallpapers/city.ts @@ -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" @@ -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[] = [] @@ -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) @@ -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) @@ -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 @@ -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) @@ -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 diff --git a/wallpapers/desert.ts b/wallpapers/desert.ts index 623b476..9260791 100644 --- a/wallpapers/desert.ts +++ b/wallpapers/desert.ts @@ -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" @@ -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() @@ -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) } diff --git a/wallpapers/farm.ts b/wallpapers/farm.ts index 3f5319d..a73f579 100644 --- a/wallpapers/farm.ts +++ b/wallpapers/farm.ts @@ -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" @@ -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. @@ -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() diff --git a/wallpapers/prehistoric.ts b/wallpapers/prehistoric.ts index a204adf..5facd1d 100644 --- a/wallpapers/prehistoric.ts +++ b/wallpapers/prehistoric.ts @@ -3,7 +3,7 @@ import { bob, body, chain, gait, limb, quadruped, stepGait, stride, type Gait } import { eggWait } from "../src/egg" import { haze, mottle } from "../src/grade" import { bird, flyAway, startle, type Flier } from "../src/flock" -import { lightPool } from "../src/light" +import { LightMap } 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" @@ -206,6 +206,7 @@ class Prehistoric extends Canvas { // shows (none outside the water or under the plants in front of it). private pool = { y0: 0, y1: 0, rows: new Float32Array(0), amp: new Float32Array(0), gloss: new Float32Array(0) } private readonly weather: WeatherLayer + private lights = new LightMap() private storm = makeStorm() private birds: Flier[] = [] // The easter egg: a time-travelling car. t counts scene seconds since it appeared at x0. @@ -300,8 +301,18 @@ class Prehistoric extends Canvas { this.drawDust() this.drawRex() const A = this.A - // At night the lava lights the ground, the lake and anything walking near the volcano's foot. - if (this.night) lightPool(this.hdr, this.W, this.H, (A - VENT_X + 0.08) * this.H, 0.63 * this.H, 0.45 * this.H, 0.13 * this.H, [1, 0.36, 0.06], 2.4 + 0.4 * Math.sin(this.time * 0.6), 0.03) + // At night the lava lights the ground, the lake and anything walking near the volcano's foot, swelling as each + // pulse of heat reaches the bottom of the flows and reaching the lake a moment later. + this.lights.clear(this.W, this.H) + if (this.night) { + const pulse = (lag: number) => 0.5 + 0.5 * Math.sin(this.time * 0.6 - lag) + this.lights.pool((A - VENT_X + 0.08 + 0.02 * Math.sin(this.time * 0.25)) * this.H, 0.63 * this.H, 0.45 * this.H, 0.13 * this.H, [1, 0.36, 0.06], 2.1 + 0.7 * pulse(0), 0.03) + if (this.activity === "teeming") { + const [lx, ly, lrx, lry] = this.lake() + this.lights.pool((Math.max(lx, A - VENT_X - 0.05) + 0.03 * Math.sin(this.time * 0.3)) * this.H, ly * this.H, lrx * 0.7 * this.H, lry * 1.6 * this.H, [1, 0.3, 0.05], 1.6 * pulse(0.9), 0.02 * pulse(0.9)) + } + } + this.lights.apply(this.hdr) for (const [i, x] of [0.01, 0.2, 0.76, 0.99].entries()) this.drawFern(x * A, 1.03, 0.17 + hash(i) * 0.04, i, true) if (this.activity === "teeming") for (const d of this.dragonflies) this.drawDragonfly(d) if (this.delorean.t >= 0) this.drawDelorean() diff --git a/wallpapers/tundra.ts b/wallpapers/tundra.ts index 0cc408f..282d5dc 100644 --- a/wallpapers/tundra.ts +++ b/wallpapers/tundra.ts @@ -2,8 +2,8 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { bob, body, chain, gait, quadruped, stepGait, stride, view, type Gait } from "../src/creature" import { eggWait } from "../src/egg" import { haze, mottle } from "../src/grade" -import { lightPool } from "../src/light" -import { TAU, clamp, fbm1, hash, lerp, rand, smoothstep, type RGB } from "../src/math" +import { LightMap, firelight } from "../src/lightmap" +import { TAU, clamp, fbm1, hash, 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" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" @@ -158,6 +158,7 @@ class Tundra extends Canvas { private readonly weather: WeatherLayer // The big pines framing the edges, painted once, bend a little as gusts pass. private pines = new SwayLayer() + private lights = new LightMap() constructor(settings: Settings) { super() @@ -251,9 +252,17 @@ class Tundra extends Canvas { if (this.activity === "teeming") this.drawFisher() for (const p of [...this.penguins].sort((a, b) => a.y - b.y)) this.drawPenguin(p) if (this.fox) this.drawFox(this.fox) - // At night the igloo doors' warm light falls across the snow and anything passing them. + // The aurora's light ripples over the snow and ice below it, and at night the igloo doors' firelight flickers + // across the snow and anything passing them. + this.lights.clear(this.W, this.H) + if (this.look.aurora > 0 && !this.weather.covered) this.lightSnow() if (this.look.glow === 1) - for (const [x, base, R] of this.igloos()) lightPool(this.hdr, this.W, this.H, (x - R * 0.35) * this.H, (base + 0.01) * this.H, R * 3.2 * this.H, R * 1.1 * this.H, [1, 0.42, 0.08], 1, 0.03) + for (const [i, [x, base, R]] of this.igloos().entries()) { + const k = firelight(this.time, i) + const sway = (noise1(this.time * 0.9, i + 7) - 0.5) * 0.3 * R + this.lights.pool((x - R * 0.35 + sway) * this.H, (base + 0.01) * this.H, R * 3.2 * this.H, R * 1.1 * this.H, [1, 0.42, 0.08], 1.3 * k, 0.036 * k) + } + this.lights.apply(this.hdr) this.drawFlakes() this.weather.draw(this.hdr, this.W, this.H) this.finish() @@ -500,7 +509,7 @@ class Tundra extends Canvas { for (let x = 0; x < W; x++) { const u = x / H const band = (0.17 + 0.05 * Math.sin(u * 1.7 + t * 0.07) + 0.03 * Math.sin(u * 4.1 - t * 0.05)) * H - const fold = 0.35 + 0.65 * Math.pow(0.5 + 0.5 * Math.sin(u * 14 + Math.sin(u * 3 + t * 0.1) * 3 + t * 0.15), 2) + const fold = auroraFold(u, t) bottom = Math.max(bottom, band + 0.05 * H) for (let y = 0; y < Math.min(H, band + 0.05 * H); y++) { const above = band - y @@ -516,6 +525,29 @@ class Tundra extends Canvas { this.touch(0, 0, W - 1, bottom) } + // The aurora's green and violet light on the snowfield, strongest under its brightest folds and on the ice, rippling + // slowly as the curtains above fold and drift. + private lightSnow() { + const { H, A, lights } = this + const t = this.time + const igloos = this.igloos() + for (let i = 0; i < lights.cols; i++) { + const u = (i * lights.cell) / H + const fold = (auroraFold(u - 0.04, t) + auroraFold(u, t) + auroraFold(u + 0.04, t)) / 3 + const violet = 0.75 * smoothstep(0.1, 0.9, Math.sin(u * 3.5 - t * 0.05 + 1)) + const color: RGB = [lerp(0.05, 0.5, violet), lerp(0.8, 0.08, violet), lerp(0.3, 0.75, violet)] + for (let j = Math.floor((HORIZON * H) / lights.cell); j < lights.rows; j++) { + const v = (j * lights.cell) / H + const lake = 1 + 0.9 * smoothstep(1.2, 0.6, ((u - 0.5 * A) / 0.3) ** 2 + ((v - 0.745) / 0.045) ** 2) + const ripple = 0.65 + 0.35 * Math.sin(u * 6 + v * 38 - t * 0.25) + // Firelight wins around the igloo doors, so it stays warm rather than mixing toward white. + const fire = igloos.reduce((m, [x, base, R]) => Math.max(m, smoothstep(1, 0.2, ((u - x + R * 0.35) / (R * 3)) ** 2 + ((v - base) / (R * 1.4)) ** 2)), 0) + const k = fold * ripple * lake * (1 - 0.85 * fire) * smoothstep(HORIZON, 0.63, v) * (1 - 0.7 * smoothstep(0.66, 0.95, v)) * this.look.aurora + if (k > 0.01) lights.node(i, j, color, 0.9 * k, 0.009 * k) + } + } + } + private drawFlakes() { const { H, look } = this for (const f of this.flakes) { @@ -763,6 +795,11 @@ class Tundra extends Canvas { } } +// How bright the aurora's curtain is at u across the screen (screen heights) at scene time t, as it folds and drifts. +function auroraFold(u: number, t: number) { + return 0.35 + 0.65 * Math.pow(0.5 + 0.5 * Math.sin(u * 14 + Math.sin(u * 3 + t * 0.1) * 3 + t * 0.15), 2) +} + function lerpRGB(a: RGB, b: RGB, t: number): RGB { return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)] } diff --git a/wallpapers/zen.ts b/wallpapers/zen.ts index 4cf1c35..b2125df 100644 --- a/wallpapers/zen.ts +++ b/wallpapers/zen.ts @@ -3,7 +3,8 @@ import { limb } from "../src/creature" import { eggWait } from "../src/egg" import { haze, mottle } from "../src/grade" import { bird, flyAway, startle, type Flier } from "../src/flock" -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, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky" @@ -235,6 +236,7 @@ class Zen extends Canvas { // under a light snowfall in winter. private readonly season: Season private readonly weather: WeatherLayer + private lights = new LightMap() private storm = makeStorm() private sparrows: Flier[] = [] // The easter egg: a tanuki with a leaf on its head. t counts scene seconds since it appeared. @@ -334,8 +336,14 @@ class Zen extends Canvas { if (this.tanuki.t >= 0) this.drawTanuki() for (const p of this.petals) if (p.state !== 1) this.drawPetal(p) for (const b of this.sparrows) this.shape(bird(b, this.H, 0.02 * this.H, this.paint([0.45, 0.33, 0.22])), this.lighting, 0.4) - // At night the lanterns light the gravel, the bank and whatever stands near them. - if (this.look.night) for (const l of this.lanterns()) lightPool(this.hdr, this.W, this.H, l.x * this.H, l.base * this.H, l.scale * 1.6 * this.H, l.scale * 0.6 * this.H, [1, 0.5, 0.15], 2, 0.01) + // At night the lanterns' candles light the gravel, the bank and whatever stands near them, flickering softly. + this.lights.clear(this.W, this.H) + if (this.look.night) + for (const [i, l] of this.lanterns().entries()) { + const k = 0.3 + 0.7 * firelight(this.time * 0.7, i + 2) + this.lights.pool(l.x * this.H, l.base * this.H, l.scale * 1.6 * this.H, l.scale * 0.6 * this.H, [1, 0.5, 0.15], 2.1 * k, 0.0105 * k) + } + this.lights.apply(this.hdr) this.drawFireflies() this.weather.draw(this.hdr, this.W, this.H) this.finish()