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: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ sunset scenes by about a fifth and night scenes barely, so night keeps its color
Night must not use white or gray highlights, because OpenCode's text is white. Use saturated, tinted colors instead:
cobalt and teal moonlight, a gold moon, amber lamps and windows, tinted stars, glowing plankton or fireflies.

Give night clear light structure so things stay readable: a lighter band at the horizon (or, underwater, toward 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.

## Seasons and weather

`settings.season` is spring, summer, autumn or winter, or missing for the classic look. Change what the season would
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 { campfire, fireflyLight, lightPool } from "./light"
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
60 changes: 60 additions & 0 deletions src/light.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { cap, type Part } from "./canvas"
import type { RGB } from "./math"

// A pool of light from a lantern, lava, a window or glowing plankton, falling off over an ellipse (pixels) around
// (cx, cy). lift lights what is already there in the light's own color, in proportion to its brightest channel, so lit
// ground and creatures keep their shapes and take on the light's hue; glow adds a little haze of the color itself.
export function lightPool(hdr: Float32Array, W: number, H: number, cx: number, cy: number, rx: number, ry: number, color: RGB, lift: number, glow: number) {
const x0 = Math.max(0, Math.floor(cx - rx))
const x1 = Math.min(W - 1, Math.ceil(cx + rx))
const y0 = Math.max(0, Math.floor(cy - ry))
const y1 = Math.min(H - 1, Math.ceil(cy + ry))
const lr = color[0] * lift
const lg = color[1] * lift
const lb = color[2] * lift
const gr = color[0] * glow
const gg = color[1] * glow
const gb = color[2] * glow
for (let y = y0; y <= y1; y++) {
const dy = (y + 0.5 - cy) / ry
for (let x = x0; x <= x1; x++) {
const dx = (x + 0.5 - cx) / rx
const d = 1 - dx * dx - dy * dy
if (d <= 0) continue
const f = d * d
const o = (y * W + x) * 3
// Capped, so foam, lamps and other 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 + gr) * f
hdr[o + 1] += (m * lg + gg) * f
hdr[o + 2] += (m * lb + gb) * f
}
}
}

// The little pool of yellow-green light a firefly at (x, y) throws on the leaves and grass around it, k its blink.
export function fireflyLight(hdr: Float32Array, W: number, H: number, x: number, y: number, k: number) {
lightPool(hdr, W, H, x, y, 0.045 * H, 0.045 * H, [0.75, 1, 0.25], 4 * k, 0.012 * k)
}

// A small campfire standing at (x, gy) in pixels, H pixels to a screen height: crossed logs and tongues of flame, each
// tongue its own part list so the hot core draws over the outer flames (flat, with shape light 0). flicker, about 0.8
// to 1, scales the light the fire throws.
export function campfire(x: number, gy: number, H: number, t: number) {
const flicker = 0.85 + 0.1 * Math.sin(t * 7.1) + 0.05 * Math.sin(t * 13.3 + 1)
const log: RGB = [0.05, 0.025, 0.02]
const logs = [cap(x - 0.03 * H, gy, x + 0.025 * H, gy - 0.008 * H, 0.006 * H, 0.005 * H, log), cap(x + 0.03 * H, gy, x - 0.02 * H, gy - 0.01 * H, 0.006 * H, 0.005 * H, log)]
const flames: Part[][] = (
[
[-0.008, 0.036, 0.009, false],
[0.008, 0.03, 0.008, false],
[0, 0.046, 0.011, false],
[0, 0.026, 0.006, true],
] as const
).map(([dx, h, r, core]) => {
const sway = Math.sin(t * 3 + dx * 400) * 0.003 * H
const height = h * H * (0.8 + 0.2 * Math.sin(t * 5.3 + dx * 900 + h * 100)) * flicker
return [cap(x + dx * H, gy - 0.004 * H, x + dx * H + sway, gy - height, r * H, 0.0015 * H, core ? [2.6, 1.5, 0.3] : [2, 0.6, 0.08])]
})
return { flicker, logs, flames }
}
33 changes: 26 additions & 7 deletions wallpapers/beach.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 { campfire, lightPool } from "../src/light"
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"
Expand Down Expand Up @@ -83,17 +84,18 @@ const LOOKS: Record<Time, Look> = {
},
night: {
style: "rim",
light: [0.2, 0.45, 1],
light: [0.3, 0.62, 1.5],
sky: [
[0, [0.004, 0.008, 0.03]],
[0.3, [0.012, 0.02, 0.06]],
[HORIZON, [0.04, 0.06, 0.15]],
[0.25, [0.014, 0.024, 0.07]],
[0.4, [0.035, 0.055, 0.14]],
[HORIZON, [0.07, 0.1, 0.24]],
],
orb: { x: 0.7, y: 0.16, r: 0.03, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 140,
clouds: { puffy: false, top: [0.015, 0.02, 0.05], bottom: [0.06, 0.1, 0.3], alpha: 0.4 },
sea: [
[0.03, 0.06, 0.15],
[0.045, 0.085, 0.2],
[0.01, 0.03, 0.07],
],
sand: [
Expand All @@ -102,7 +104,7 @@ const LOOKS: Record<Time, Look> = {
],
foam: [0.1, 0.85, 1.1],
glitter: 0.7,
tint: [0.025, 0.04, 0.09],
tint: [0.02, 0.032, 0.075],
night: true,
},
}
Expand Down Expand Up @@ -262,6 +264,7 @@ class Beach extends Canvas {
if (this.activity !== "calm" && !this.look.night) this.drawGulls()
if (this.activity === "teeming") this.drawCrab()
for (const b of this.startled) this.shape(bird(b, this.H, 0.03 * this.H, this.paint([0.75, 0.78, 0.82])), this.lighting, 0.5)
if (this.look.night) this.drawBonfire()
// Trunks are drawn each frame, over the sea, since they cross the water.
for (const p of this.palms) {
this.drawTrunk(p)
Expand Down Expand Up @@ -378,8 +381,8 @@ class Beach extends Canvas {
}
// At night the glowing foam lights the wet sand just beyond it.
if (look.night)
for (let y = Math.floor(edge); y < Math.min(H, edge + 0.02 * H); y++) {
const k = Math.exp(-(y - edge) / 2.5) * 0.15
for (let y = Math.floor(edge); y < Math.min(H, edge + 0.05 * H); y++) {
const k = Math.exp(-(y - edge) / (0.012 * H)) * 0.2
const o = (y * W + x) * 3
hdr[o] += look.foam[0] * k
hdr[o + 1] += look.foam[1] * k
Expand All @@ -388,6 +391,17 @@ class Beach extends Canvas {
}
}

// A bonfire on the sand at night, lighting the beach, the crab and the palm beside it.
private drawBonfire() {
const H = this.H
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)
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)
}

// A palm trunk curving from its base to the crown, ringed with old leaf scars, with coconuts at the top.
private drawTrunk(p: Palm) {
const H = this.H
Expand Down Expand Up @@ -525,6 +539,11 @@ class Beach extends Canvas {
this.polygon([[x - s * 0.6, y - s * 0.12], [x + s * 0.6, y - s * 0.12], [x + s * 0.42, y + s * 0.05], [x - s * 0.42, y + s * 0.05]], this.paint([0.3, 0.2, 0.15]))
this.polygon([[x - s * 0.05, y - s * 0.15], [x - s * 0.05, y - s * 1.1], [x + s * 0.55 * b.dir, y - s * 0.15]], this.paint([0.92, 0.9, 0.84]))
this.polygon([[x - s * 0.12, y - s * 0.15], [x - s * 0.12, y - s * 0.85], [x - s * 0.5 * b.dir, y - s * 0.15]], this.paint([0.85, 0.82, 0.76]))
// At night a lantern hangs at the stern, lighting the sails and the water around the boat.
if (!this.look.night) return
const lx = x - s * 0.45 * b.dir
lightPool(this.hdr, this.W, H, lx, y, s * 1.4, s * 0.9, [1, 0.55, 0.15], 2.5, 0.03)
this.disc(lx, y - s * 0.25, Math.max(1, s * 0.07), 1.6, 0.85, 0.2, 1)
}

// Each dolphin waits underwater, then arcs out of the sea and splashes back in.
Expand Down
10 changes: 7 additions & 3 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 { lightPool } from "../src/light"
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 @@ -93,11 +94,12 @@ const LOOKS: Record<Time, Look> = {
},
night: {
style: "rim",
light: [0.25, 0.3, 1],
light: [0.35, 0.42, 1.4],
sky: [
[0, [0.004, 0.006, 0.03]],
[0.32, [0.014, 0.016, 0.06]],
[BANK, [0.1, 0.035, 0.14]],
[0.32, [0.016, 0.018, 0.07]],
[0.45, [0.05, 0.025, 0.12]],
[BANK, [0.15, 0.05, 0.2]],
],
orb: { x: 0.82, y: 0.12, r: 0.03, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 50,
Expand Down Expand Up @@ -310,6 +312,8 @@ class City extends Canvas {
const a = Math.floor((ROAD + 0.012) * H) * W * 3
this.hdr.set(this.background.subarray(a, Math.ceil((ROAD + 0.026) * H) * W * 3), a)
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)
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
31 changes: 22 additions & 9 deletions wallpapers/desert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 { TAU, clamp, fbm1, fbm2, hash, lerp, rand, smoothstep, type RGB } from "../src/math"
import { campfire, lightPool } from "../src/light"
import { driftClouds, makeStorm, paintStorm } from "../src/sky"
import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper"
import { WeatherLayer } from "../src/weather"
Expand Down Expand Up @@ -139,12 +140,12 @@ const LOOKS: Record<Time, Look> = {
},
night: {
style: "rim",
light: [0.18, 0.42, 1],
light: [0.28, 0.6, 1.45],
sky: [
[0, [0.004, 0.006, 0.02]],
[0.3, [0.01, 0.018, 0.05]],
[0.5, [0.025, 0.04, 0.09]],
[HORIZON, [0.05, 0.07, 0.13]],
[0.3, [0.012, 0.02, 0.06]],
[0.5, [0.035, 0.055, 0.13]],
[HORIZON, [0.08, 0.11, 0.24]],
],
orb: { x: 0.3, y: 0.16, r: 0.035, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 160,
Expand All @@ -153,11 +154,11 @@ const LOOKS: Record<Time, Look> = {
farMesa: [0.03, 0.04, 0.08],
nearMesa: [0.012, 0.016, 0.035],
floor: [
[0.05, 0.06, 0.1],
[0.015, 0.018, 0.035],
[0.07, 0.09, 0.17],
[0.025, 0.03, 0.06],
],
backDune: [0.025, 0.03, 0.055],
frontDune: [0.012, 0.014, 0.028],
backDune: [0.035, 0.045, 0.095],
frontDune: [0.02, 0.025, 0.055],
cactus: [0.01, 0.02, 0.02],
bone: [0.1, 0.2, 0.42],
horn: [0.08, 0.13, 0.28],
Expand All @@ -175,7 +176,7 @@ const LOOKS: Record<Time, Look> = {
],
twig: [0.06, 0.09, 0.2],
night: true,
plumage: [0.08, 0.075, 0.08],
plumage: [0.05, 0.06, 0.12],
plumageLight: [0.08, 0.1, 0.22],
flock: [0.02, 0.02, 0.03],
dust: [0, 0, 0],
Expand Down Expand Up @@ -349,6 +350,7 @@ class Desert extends Canvas {
if (this.roadrunner.t >= 0) this.drawRoadrunner()
for (const b of this.startled) this.silhouette(bird(b, this.H, 0.022 * this.H, this.look.flock), 0.4)
this.drawTumbleweed()
if (this.look.night) this.drawCampfire()
this.drawDust()
this.weather.draw(this.hdr, this.W, this.H)
this.finish()
Expand Down Expand Up @@ -657,6 +659,17 @@ class Desert extends Canvas {
this.shape(parts, lighting, light)
}

// A small campfire on the dunes at night, its warm light falling on the sand and the saguaro beside it.
private drawCampfire() {
const H = this.H
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)
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)
}

// A saguaro: a ribbed trunk with two upturned arms. haze fades distant ones toward the dunes behind them.
private drawSaguaro(x: number, gy: number, h: number, haze: number) {
const c = mix(this.look.cactus, this.look.backDune, haze * 0.6)
Expand Down
19 changes: 12 additions & 7 deletions wallpapers/farm.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 { fireflyLight, lightPool } from "../src/light"
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"
Expand Down Expand Up @@ -68,20 +69,21 @@ const LOOKS: Record<Time, Look> = {
},
night: {
style: "rim",
light: [0.2, 0.45, 1],
light: [0.3, 0.62, 1.5],
sky: [
[0, [0.004, 0.008, 0.03]],
[0.3, [0.012, 0.02, 0.06]],
[HORIZON, [0.04, 0.06, 0.15]],
[0.3, [0.014, 0.024, 0.07]],
[0.45, [0.035, 0.055, 0.14]],
[HORIZON, [0.07, 0.1, 0.24]],
],
orb: { x: 0.74, y: 0.15, r: 0.032, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 140,
clouds: { puffy: false, top: [0.015, 0.02, 0.05], bottom: [0.06, 0.1, 0.3], alpha: 0.45 },
hills: [
[0.02, 0.035, 0.07],
[0.008, 0.016, 0.03],
[0.035, 0.06, 0.13],
[0.016, 0.03, 0.065],
],
tint: [0.025, 0.04, 0.08],
tint: [0.02, 0.032, 0.065],
windows: 1,
fireflies: true,
},
Expand Down Expand Up @@ -273,6 +275,8 @@ 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, 0.74 * this.A * this.H, 0.67 * this.H, 0.24 * this.H, 0.07 * this.H, [1, 0.55, 0.15], 3, 0.02)
this.drawCornRow(3)
this.drawFireflies()
this.drawLeaves()
Expand Down Expand Up @@ -414,7 +418,7 @@ class Farm extends Canvas {
this.polygon(P([[c - 0.077, b - 0.157], [c, b - 0.188], [c + 0.077, b - 0.157], [c + 0.072, b - 0.148], [c, b - 0.176], [c - 0.072, b - 0.148]]), snow)
this.shape([ell((c - 0.13) * H, (b - 0.175) * H, 0.023 * H, 0.013 * H, 0, snow)], this.lighting, 0.4)
}
this.polygon(P([[c - 0.035, b - 0.065], [c + 0.035, b - 0.065], [c + 0.035, b], [c - 0.035, b]]), this.paint([0.42, 0.07, 0.05]))
this.polygon(P([[c - 0.035, b - 0.065], [c + 0.035, b - 0.065], [c + 0.035, b], [c - 0.035, b]]), this.look.windows === 1 ? [1.1, 0.6, 0.18] : this.paint([0.42, 0.07, 0.05]))
const trim = this.paint([0.88, 0.86, 0.8])
const t = 0.0025 * H
this.shape(
Expand Down Expand Up @@ -871,6 +875,7 @@ class Farm extends Canvas {
for (const f of this.fireflies) {
const k = Math.pow(Math.max(0, Math.sin(this.time * 1.3 + f.phase * 7)), 6)
if (k < 0.02) continue
fireflyLight(this.hdr, this.W, H, f.x * H, f.y * H, k)
this.disc(f.x * H, f.y * H, 1.3, 1.8 * k, 2.4 * k, 0.6 * k, 0.6)
this.add(f.x * H, f.y * H, 0.9 * k, 1.2 * k, 0.3 * k)
}
Expand Down
10 changes: 6 additions & 4 deletions wallpapers/jungle.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 { fireflyLight } from "../src/light"
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"
Expand Down Expand Up @@ -58,11 +59,11 @@ const LOOKS: Record<Time, Look> = {
},
night: {
style: "rim",
light: [0.1, 0.7, 0.62],
light: [0.14, 0.95, 0.85],
mist: [
[0.02, 0.08, 0.12],
[0.008, 0.035, 0.05],
[0.002, 0.008, 0.01],
[0.02, 0.085, 0.13],
[0.014, 0.065, 0.09],
[0.003, 0.012, 0.016],
],
far: [0.015, 0.05, 0.06],
water: [0.04, 0.38, 0.5],
Expand Down Expand Up @@ -672,6 +673,7 @@ class Jungle extends Canvas {
for (const f of this.glowers) {
const k = Math.max(together, Math.pow(Math.max(0, Math.sin(this.time * 1.3 + f.phase * 7)), 6))
if (k < 0.02) continue
fireflyLight(this.hdr, this.W, H, f.x * H, f.y * H, k)
this.disc(f.x * H, f.y * H, 1.3, 0.9 * k * 2, 1.2 * k * 2, 0.3 * k * 2, 0.6)
this.add(f.x * H, f.y * H, 0.9 * k, 1.2 * k, 0.3 * k)
}
Expand Down
Loading
Loading