From 1a1b95182993bbd8e46115fd0c31f62b4f5a50af Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 20:28:11 -0700 Subject: [PATCH 1/3] Ground creatures and props with contact and cast shadows --- src/api.ts | 1 + src/shadow.ts | 94 +++++++++++++++++++++++++++++++++++++++ wallpapers/beach.ts | 15 +++++++ wallpapers/city.ts | 12 +++++ wallpapers/desert.ts | 37 ++++++--------- wallpapers/farm.ts | 40 +++++++++++++++++ wallpapers/jungle.ts | 6 +++ wallpapers/prehistoric.ts | 27 +++++++++++ wallpapers/tundra.ts | 31 +++++++++++++ wallpapers/zen.ts | 11 +++++ 10 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 src/shadow.ts diff --git a/src/api.ts b/src/api.ts index 1815efa..5a83c64 100644 --- a/src/api.ts +++ b/src/api.ts @@ -3,6 +3,7 @@ export { Canvas, cap, ell, type Lighting, type Part } from "./canvas" export { eggWait } from "./egg" export { bird, flyAway, startle, type Flier } from "./flock" 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, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "./sky" export { ACTIVITIES, diff --git a/src/shadow.ts b/src/shadow.ts new file mode 100644 index 0000000..83ae2dd --- /dev/null +++ b/src/shadow.ts @@ -0,0 +1,94 @@ +// Shadows on the ground: a soft contact shadow right under a thing, and the shadow it casts away from the light. +// Scenes make one Shade per layout with sunShade() and call groundShadow() under each thing that stands on the ground, +// before drawing it: static things in layout(), moving things each frame. +import { clamp, type RGB } from "./math" +import type { Time } from "./wallpaper" + +// The ground is seen at a low angle, so a round footprint looks this much shallower than it is wide. +const SQUASH = 0.38 + +// How a scene's light throws shadows. Shadows multiply the ground toward tint, a dark color tinted by the sky that +// fills them, so they deepen the ground's own hue rather than graying it. +export interface Shade { + tint: RGB + // Strength of the soft patch right under a thing, and of the shadow it casts. + contact: number + cast: number + // Cast length per unit of height. + length: number + // A low sun throws shadows away from (x, y), in fractions of the width and height; a high sun or moon throws them + // all along (x, y). + x: number + y: number + radiate: boolean +} + +// Short, soft shadows by day; long ones at sunset, spreading away from the low sun; at night faint contact shadows and +// a hint of moon shadow. Under cloud the light is diffuse, so only contact shadows remain. orb is the sun or moon, in +// fractions of the width and height. +export function sunShade(time: Time, orb: { x: number; y: number }, covered = false, tint?: RGB): Shade { + const away = orb.x < 0.5 ? 1 : -1 + const looks: Record = { + day: { tint: [0.36, 0.44, 0.64], contact: 0.75, cast: 0.6, length: 0.6, x: away, y: 0.45, radiate: false }, + sunset: { tint: [0.32, 0.22, 0.42], contact: 0.7, cast: 0.75, length: 2.4, x: orb.x, y: orb.y, radiate: true }, + night: { tint: [0.35, 0.45, 0.85], contact: 0.45, cast: 0.2, length: 0.6, x: away, y: 0.45, radiate: false }, + } + const shade = looks[time] + return { ...shade, tint: tint ?? shade.tint, contact: covered ? shade.contact * 0.8 : shade.contact, cast: covered ? 0 : shade.cast } +} + +// The shadows of a thing w pixels wide and h tall standing at (x, y), the ground under its middle. tip is the width +// of the far end of its cast shadow relative to the near end: under 1 for things that narrow upward, over 1 for a tree +// with a wide crown. lift is how far it is off the ground in pixels: the contact shadow spreads and fades, and the cast +// shadow slides away. +export function groundShadow(hdr: Float32Array, W: number, H: number, shade: Shade, x: number, y: number, w: number, h: number, tip = 0.7, lift = 0) { + const [tr, tg, tb] = shade.tint + const up = clamp(lift / Math.max(1, h * 0.5), 0, 1) + // Centered a little in front of the feet: the half behind them is mostly hidden by the thing itself. + const r = w * 0.6 * (1 + up * 0.6) + const cy = y + r * SQUASH * 0.3 + if (shade.contact > 0 && up < 1) fillShadow(hdr, W, H, x, cy, x, cy, r, r, tr, tg, tb, shade.contact * (1 - up) ** 2, 1, 0.6) + if (shade.cast <= 0) return + const dx = shade.radiate ? x - shade.x * W : shade.x + const dy = shade.radiate ? Math.max(0, y - shade.y * H) : shade.y + const d = Math.hypot(dx, dy) || 1 + // Toward the viewer the ground is foreshortened, so the down-screen part of a shadow is shorter. + const ux = dx / d + const uy = (dy / d) * 0.5 + const near = lift * shade.length + const far = near + h * shade.length + fillShadow(hdr, W, H, x + ux * near, y + uy * near, x + ux * far, y + uy * far, w * 0.4, w * 0.4 * tip, tr, tg, tb, shade.cast * (1 - up * 0.5), 0.3, 0.55) +} + +// Darkens a soft tapered capsule lying on the ground toward (tr, tg, tb): from (ax, ay) radius r0 to (bx, by) radius +// r1, in pixels, measured with y stretched by 1/SQUASH. Strength a at the near end fades to a * fade at the far end. +// soft is the share of the radius that is penumbra at the near end (1 fades from the middle); it widens to 1 at the far +// end. +export function fillShadow(hdr: Float32Array, W: number, H: number, ax: number, ay: number, bx: number, by: number, r0: number, r1: number, tr: number, tg: number, tb: number, a: number, fade: number, soft: number) { + if (a <= 0) return + const R = Math.max(r0, r1) + const x0 = Math.max(0, Math.floor(Math.min(ax, bx) - R)) + const x1 = Math.min(W - 1, Math.ceil(Math.max(ax, bx) + R)) + const y0 = Math.max(0, Math.floor(Math.min(ay, by) - R * SQUASH)) + const y1 = Math.min(H - 1, Math.ceil(Math.max(ay, by) + R * SQUASH)) + const dx = bx - ax + const dy = (by - ay) / SQUASH + const len2 = dx * dx + dy * dy + for (let y = y0; y <= y1; y++) { + const oy = (y + 0.5 - ay) / SQUASH + for (let x = x0; x <= x1; x++) { + const ox = x + 0.5 - ax + const t = len2 > 0 ? clamp((ox * dx + oy * dy) / len2, 0, 1) : 0 + const r = r0 + (r1 - r0) * t + const e = Math.hypot(ox - dx * t, oy - dy * t) / r + if (e >= 1) continue + // The penumbra widens along the shadow, away from what casts it. + const k = clamp((1 - e) / (soft + (1 - soft) * t), 0, 1) + const s = a * k * k * (3 - 2 * k) * (1 + (fade - 1) * t) + const i = (y * W + x) * 3 + hdr[i] *= 1 - s * (1 - tr) + hdr[i + 1] *= 1 - s * (1 - tg) + hdr[i + 2] *= 1 - s * (1 - tb) + } + } +} diff --git a/wallpapers/beach.ts b/wallpapers/beach.ts index f977204..37bad4c 100644 --- a/wallpapers/beach.ts +++ b/wallpapers/beach.ts @@ -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, 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" import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -163,6 +164,7 @@ class Beach extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private stars: Star[] private clouds: Cloud[] @@ -190,6 +192,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 + this.shade = sunShade(settings.time, look.orb, this.weather.covered) 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) @@ -312,6 +315,11 @@ class Beach extends Canvas { this.background = this.hdr.slice() } + private ground(x: number, y: number, w: number, h: number, tip?: number, lift?: number) { + const H = this.H + groundShadow(this.hdr, this.W, H, this.shade, x * H, y * H, w * H, h * H, tip, lift) + } + private paint(c: RGB): RGB { const t = this.look.tint return [c[0] * t[0], c[1] * t[1], c[2] * t[2]] @@ -453,6 +461,8 @@ class Beach extends Canvas { this.shape([cap(...P(-0.035, 0.06), ...P(0.045, 0.056), 0.009 * s * H, 0.006 * s * H, c), ell(...P(-0.048, 0.059), 0.01 * s * H, 0.009 * s * H, 0, c)], this.lighting, 0.6) this.shape([cap(...P(-0.012, 0.06), ...P(0.012, 0.059), 0.0095 * s * H, 0.0095 * s * H, this.paint(stripe))], this.lighting, 0.4) } + // The canopy shades whoever lies under it. + this.ground(ux, uy + 0.05 * s, 0.18 * s, 0.17 * s, 1.2) this.shape([cap(...P(0, 0.05), ...P(0, -0.12), 0.003 * s * H, 0.003 * s * H, this.paint([0.85, 0.85, 0.82]))], this.lighting, 0.4) for (let k = 0; k < 6; k++) { const a0 = Math.PI + (k / 6) * Math.PI @@ -466,6 +476,7 @@ class Beach extends Canvas { private drawDriftwood(x: number, y: number) { const H = this.H const wood = this.paint([0.62, 0.55, 0.45]) + this.ground(x, y + 0.006, 0.13, 0.02, 1) this.shape([cap((x - 0.07) * H, y * H, (x + 0.06) * H, (y - 0.008) * H, 0.009 * H, 0.006 * H, wood, 2), cap((x + 0.02) * H, (y - 0.005) * H, (x + 0.05) * H, (y - 0.03) * H, 0.004 * H, 0.002 * H, wood)], this.lighting, 0.7) } @@ -485,6 +496,8 @@ class Beach extends Canvas { if (this.season !== "summer") this.drawUmbrella(0.64 * A, 0.86, [0.85, 0.15, 0.12]) const [sx, sy] = [0.42 * A, 0.92] const sand = this.paint([0.78, 0.66, 0.42]) + this.ground(sx, sy, 0.1, 0.09, 0.5) + this.ground(0.53 * A, 0.95, 0.028, 0.032) const box = (x0: number, y0: number, x1: number, y1: number) => [[x0 * H, y0 * H], [x1 * H, y0 * H], [x1 * H, y1 * H], [x0 * H, y1 * H]] as const this.polygon(box(sx - 0.05, sy - 0.03, sx + 0.05, sy), sand) for (const [dx, h] of [[-0.04, 0.06], [0, 0.08], [0.04, 0.06]] as const) { @@ -626,6 +639,7 @@ class Beach extends Canvas { const y = lerp(lerp(0.62, beached, come), 0.66, smoothstep(52, 62, t)) + Math.sin(this.time * 1.4) * 0.004 * clamp(afloat, 0, 1) const tilt = 0.35 + Math.sin(this.time * 1.1) * 0.25 * clamp(afloat, 0, 1) const S = 0.09 * H + if (afloat < 1) this.ground(x, y + 0.012, 0.07, 0.025, 1, clamp(afloat, 0, 1) * 0.03 * H) const ca = Math.cos(tilt) const sa = Math.sin(tilt) const P = (u: number, v: number): [number, number] => [x * H + (u * ca - v * sa) * S, y * H + (u * sa + v * ca) * S] @@ -651,6 +665,7 @@ class Beach extends Canvas { const y = 0.97 * H const red = this.paint([0.85, 0.25, 0.12]) const step = c.rest > 0 ? 0 : Math.sin(c.phase) * 0.06 + this.ground(c.x, 0.97, 0.055, 0.035) const parts: Part[] = [ell(x, y - 0.25 * S, 0.4 * S, 0.22 * S, 0, red)] for (const side of [-1, 1]) for (let k = 0; k < 3; k++) { diff --git a/wallpapers/city.ts b/wallpapers/city.ts index c2fb6c8..60df5c2 100644 --- a/wallpapers/city.ts +++ b/wallpapers/city.ts @@ -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, 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" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -215,6 +216,7 @@ class City extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) // The river with the skyline reflected in it, which each frame ripples sideways where `water` is set. private river = new Float32Array(0) @@ -251,6 +253,7 @@ class City extends Canvas { 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) + this.shade = sunShade(settings.time, this.look.orb, this.weather.covered) this.wet = settings.weather ? settings.weather === "rain" || settings.weather === "snow" : this.look.night && settings.activity === "teeming" if (settings.activity !== "teeming") return const umbrellas: RGB[] = [[0.1, 0.55, 0.6], [0.6, 0.1, 0.4], [0.65, 0.35, 0.05], [0.3, 0.15, 0.6], [0.6, 0.1, 0.08]] @@ -304,11 +307,13 @@ class City extends Canvas { this.drawLights() if (this.activity !== "calm") this.drawTrain() if (this.activity === "teeming") for (const b of this.boats) this.drawBoat(b) + for (const c of this.cars) this.ground(c.x, LANES[c.lane].y, (c.bus ? 0.08 : 0.04) * LANES[c.lane].scale, (c.bus ? 0.023 : 0.015) * LANES[c.lane].scale, 1) for (const c of this.cars) if (c.lane === 0) this.drawCar(c) for (const c of this.cars) if (c.lane === 1) this.drawCar(c) // The barrier sits in front of the near lane, so it is copied back over the cars. 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.ground(w.x, 0.996, 0.02, 0.1, 0.8) for (const w of this.walkers) this.drawWalker(w) 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() @@ -378,6 +383,11 @@ class City extends Canvas { for (let i = 0; i < 400; i++) this.stepTraffic(0.25) } + private ground(x: number, y: number, w: number, h: number, tip?: number) { + const H = this.H + groundShadow(this.hdr, this.W, H, this.shade, x * H, y * H, w * H, h * H, tip) + } + private paint(c: RGB): RGB { const t = this.look.tint return [c[0] * t[0], c[1] * t[1], c[2] * t[2]] @@ -632,6 +642,7 @@ class City extends Canvas { const leaves = this.paint([0.22, 0.42, 0.18]) for (let x = 0.29; x < A + 0.05; x += 0.34) { const base = QUAY + 0.014 + this.ground(x, base, 0.03, 0.05, 1.3) this.shape([cap(x * H, base * H, x * H, (base - 0.022) * H, 0.002 * H, 0.0015 * H, this.paint([0.35, 0.25, 0.16]))], this.lighting, 0.4) this.shape([ell(x * H, (base - 0.036) * H, 0.014 * H, 0.013 * H, 0, leaves), ell((x - 0.011) * H, (base - 0.026) * H, 0.011 * H, 0.009 * H, 0, leaves), ell((x + 0.011) * H, (base - 0.027) * H, 0.011 * H, 0.009 * H, 0, leaves)], this.lighting, 0.7) } @@ -643,6 +654,7 @@ class City extends Canvas { 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) + this.ground(x, STREET, 0.045, STREET - UNDER, 1) this.polygon([[(x - 0.028) * H, UNDER * H], [(x + 0.028) * H, UNDER * H], [(x + 0.02) * H, STREET * H], [(x - 0.02) * H, STREET * H]], this.paint(CONCRETE)) this.polygon([[(x + 0.012) * H, UNDER * H], [(x + 0.028) * H, UNDER * H], [(x + 0.02) * H, STREET * H], [(x + 0.008) * H, STREET * H]], this.paint([0.42, 0.41, 0.4])) } diff --git a/wallpapers/desert.ts b/wallpapers/desert.ts index 570ddf3..9976ad0 100644 --- a/wallpapers/desert.ts +++ b/wallpapers/desert.ts @@ -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 { groundShadow, sunShade, type Shade } from "../src/shadow" import { driftClouds, makeStorm, paintStorm } from "../src/sky" import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -278,6 +279,7 @@ class Desert extends Canvas { private readonly flowerLight: RGB private readonly dusting: RGB private readonly weather: WeatherLayer + private readonly shade: Shade constructor(settings: Settings) { super() @@ -287,6 +289,7 @@ class Desert extends Canvas { this.flowerLight = FLOWER_LIGHT[settings.time] this.dusting = DUSTING[settings.time] this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, HORIZON) + this.shade = sunShade(settings.time, this.look.orb, this.weather.covered) const look = this.look const top = look.milkyWay ? 0.5 : 0.3 this.stars = Array.from({ length: look.stars }, () => ({ x: Math.random(), y: Math.random() * top, b: 0.4 + Math.random() * 0.6, phase: Math.random() * TAU, tint: STAR_TINTS[Math.floor(Math.random() * STAR_TINTS.length)] })) @@ -398,18 +401,19 @@ class Desert extends Canvas { if (this.activity === "teeming") { for (const fx of [0.47, 0.92]) { const gy = backTop[Math.min(W - 1, Math.round(fx * W))] + 2 - this.shadow(fx * A * H, gy, 0.05 * H) + const h = fx < 0.5 ? 0.14 : 0.11 + this.shadow(fx * A * H, gy, 0.25 * h * H, h * H) this.drawSaguaro(fx * A * H, gy, (fx < 0.5 ? 0.14 : 0.11) * H, 0.5) } } - this.shadow(0.13 * A * H, this.ground(0.13), 0.12 * H) + this.shadow(0.13 * A * H, this.ground(0.13), 0.085 * H, 0.34 * H) this.drawSaguaro(0.13 * A * H, this.ground(0.13) + 3, 0.34 * H, 0) - this.shadow(0.8 * A * H, this.ground(0.8), 0.08 * H) + this.shadow(0.8 * A * H, this.ground(0.8), 0.07 * H, 0.1 * H, 2.5) this.drawSkull(0.8 * A * H, this.ground(0.8), 0.07 * H) if (this.activity === "teeming") { - this.shadow(0.33 * A * H, this.ground(0.33), 0.05 * H) + this.shadow(0.33 * A * H, this.ground(0.33), 0.09 * H, 0.16 * H, 1.1) this.drawPricklyPear(0.33 * A * H, this.ground(0.33) + 2, H) - this.shadow(0.62 * A * H, this.ground(0.62), 0.035 * H) + this.shadow(0.62 * A * H, this.ground(0.62), 0.06 * H, 0.07 * H, 0.8) this.drawBarrel(0.62 * A * H, this.ground(0.62) + 1, H) } // The coyote sits on the tallest near mesa. @@ -543,23 +547,9 @@ class Desert extends Canvas { } } - // By day, a soft shadow on the ground falling to the lower left of something standing at (x, gy). - private shadow(x: number, gy: number, length: number) { - if (this.look.style !== "front") return - const { W, H, hdr } = this - const cx = x - length * 0.55 - const rx = length * 0.7 - const ry = Math.max(1.5, length * 0.12) - for (let y = Math.max(0, Math.floor(gy - ry)); y <= Math.min(H - 1, Math.ceil(gy + ry)); y++) - for (let px = Math.max(0, Math.floor(cx - rx)); px <= Math.min(W - 1, Math.ceil(cx + rx)); px++) { - const q = ((px + 0.5 - cx) / rx) ** 2 + ((y + 0.5 - gy) / ry) ** 2 - if (q >= 1) continue - const k = 1 - 0.45 * (1 - q) ** 0.7 - const o = (y * W + px) * 3 - hdr[o] *= k - hdr[o + 1] *= k - hdr[o + 2] *= k - } + // The shadows of something w pixels wide and h tall standing at (x, gy). + private shadow(x: number, gy: number, w: number, h: number, tip?: number, lift?: number) { + groundShadow(this.hdr, this.W, this.H, this.shade, x, gy, w, h, tip, lift) } private stepTumbleweed(dt: number) { @@ -920,6 +910,7 @@ class Desert extends Canvas { const x = r.x * H const gy = this.ground(r.x / this.A) + 1 const S = 0.14 * H + this.shadow(x - 0.05 * S, gy, 0.5 * S, 0.6 * S, 0.6) const P = (u: number, v: number): [number, number] => [x + u * S, gy + v * S] const bob = running ? 0 : Math.max(0, Math.sin((r.t - r.halt) * 12)) * 0.05 const tail = running ? -0.47 : -0.78 @@ -950,7 +941,7 @@ class Desert extends Canvas { const cx = t.x * H const hop = Math.abs(Math.sin(t.hop)) * 0.012 * H const gy = this.ground(t.x / this.A) - this.shadow(cx + R * 0.8, gy, R * (2 - hop / (0.012 * H))) + this.shadow(cx, gy, R * 2, R * 2, 1, hop) const cy = gy - R + 1 - hop const cs = Math.cos(t.spin) const sn = Math.sin(t.spin) diff --git a/wallpapers/farm.ts b/wallpapers/farm.ts index 868c944..058bf48 100644 --- a/wallpapers/farm.ts +++ b/wallpapers/farm.ts @@ -1,6 +1,7 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" 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" import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -169,6 +170,7 @@ class Farm extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private stars: Star[] private clouds: Cloud[] @@ -199,6 +201,7 @@ class Farm extends Canvas { this.hills = GROUND[this.season]?.[settings.time] ?? this.look.hills // Without a weather setting, winter brings a light flurry. this.weather = new WeatherLayer(settings.weather ?? (this.season === "winter" ? "snow" : "clear"), settings.time, HORIZON, !settings.weather) + this.shade = sunShade(settings.time, this.look.orb, this.weather.covered) const drifting = { spring: 10, autumn: 22, summer: 0, winter: 0 }[this.season] * { calm: 1, lively: 1.3, teeming: 1.6 }[settings.activity] const colors: RGB[] = this.season === "spring" ? [[1, 0.78, 0.86], [0.95, 0.6, 0.72]] : [[0.9, 0.45, 0.1], [0.75, 0.2, 0.07], [0.95, 0.7, 0.18]] this.leaves = Array.from({ length: Math.round(drifting) }, (_, i) => ({ x: Math.random() * 2, y: 0.4 + Math.random() * 0.55, phase: Math.random() * TAU, color: colors[i % colors.length] })) @@ -265,6 +268,7 @@ class Farm extends Canvas { const [top, bottom] = this.weather.scud ?? [this.look.clouds.top, this.look.clouds.bottom] paintClouds(this.hdr, this.W, this.H, this.clouds, top, bottom, this.look.clouds.alpha, this.look.clouds.puffy) paintStorm(this.hdr, this.W, this.H, this.storm, this.look.clouds.top, this.look.clouds.bottom, this.gloom) + this.drawShadows() this.drawFan() this.drawTractor() for (const c of this.chickens) this.drawChicken(c) @@ -326,6 +330,13 @@ class Farm extends Canvas { } } if (season === "spring") this.drawWildflowers() + const ground = (x: number, y: number, w: number, h: number, tip?: number) => groundShadow(this.hdr, W, H, this.shade, x * H, y * H, w * H, h * H, tip) + for (const t of TREES) ground(t * A, this.nearTop[clamp(Math.round(t * W), 0, W - 1)] / H + 0.012, 0.09, 0.17, 1.4) + ground(0.5 * A, this.nearTop[clamp(Math.round(0.5 * W), 0, W - 1)] / H + 0.006, 0.08, 0.066, 0.6) + ground(0.3 * A, 0.65, 0.07, 0.27, 0.4) + ground(0.74 * A, 0.655, 0.24, 0.19, 0.6) + for (const [bx, by] of [[0.58, 0.705], [0.625, 0.712]]) ground(bx * A, by + 0.02, 0.045, 0.04) + for (let x = 0.38 * A; x < A + 0.05; x += 0.07) ground(x, 0.725, 0.012, 0.045, 0.6) this.treeTops = TREES.map((t) => { const base = this.nearTop[clamp(Math.round(t * W), 0, W - 1)] / H + 0.012 this.drawTree(t * A, base) @@ -353,6 +364,35 @@ class Farm extends Canvas { this.background = this.hdr.slice() } + // Shadows under everything that walks or drives, drawn before any of it so none falls across another animal. + private drawShadows() { + const H = this.H + const ground = (x: number, y: number, w: number, h: number, tip?: number, lift?: number) => groundShadow(this.hdr, this.W, H, this.shade, x * H, y * H, w * H, h * H, tip, lift) + const t = this.tractor + if (t.x > -5) ground(t.x + t.dir * 0.04, this.roadY(t.x) + 0.004, 0.11, 0.094) + for (const c of this.chickens) { + const S = 0.05 * this.depthScale(c.y) + ground(c.x, c.y, 0.7 * S, 0.9 * S) + } + for (const p of this.pigs) { + const S = 0.08 * this.depthScale(p.y) + ground(p.x, p.y, 0.9 * S, 0.6 * S) + } + for (const c of this.cows) { + const S = 0.11 * this.depthScale(c.y) + ground(c.x + c.face * 0.1 * S, c.y, 1.1 * S, 0.7 * S, 0.8) + } + const e = this.egg + if (e.cow && e.t < 24) { + const rise = e.t < 0 ? 0 : smoothstep(13, 24, e.t) + const size = lerp(1, 0.35, rise) + const uy = lerp(0.1, e.cow.y - 0.4, smoothstep(0, 10, e.t)) + const S = 0.11 * this.depthScale(e.cow.y) * size + ground(e.cow.x + e.cow.face * 0.1 * S, e.cow.y, 1.1 * S, 0.7 * S, 0.8, rise * (e.cow.y - uy - 0.025) * H) + } + if (this.activity === "teeming") ground(this.farmer.x, 0.738, 0.04, 0.15, 0.6) + } + private paint(c: RGB): RGB { const t = this.look.tint return [c[0] * t[0], c[1] * t[1], c[2] * t[2]] diff --git a/wallpapers/jungle.ts b/wallpapers/jungle.ts index 850f37c..7dcff7a 100644 --- a/wallpapers/jungle.ts +++ b/wallpapers/jungle.ts @@ -1,6 +1,7 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" import { TAU, clamp, fbm1, hash, lerp, rand, smoothstep, type RGB } from "../src/math" +import { groundShadow, sunShade, type Shade } from "../src/shadow" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -128,6 +129,7 @@ class Jungle extends Canvas { private readonly weather: WeatherLayer private readonly rainy: boolean private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private leaves: Leaf[] = [] private groundPx = new Float32Array(0) @@ -151,6 +153,8 @@ class Jungle extends Canvas { const rainy = settings.season === "summer" || settings.season === "autumn" this.rainy = rainy const look = LOOKS[settings.time] + // Under the canopy the light is diffuse, so things on the floor get contact shadows but cast none. + this.shade = sunShade(settings.time, { x: 0.85, y: -0.15 }, true) 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 // Without a weather setting, the rainy season brings a light shower. @@ -296,8 +300,10 @@ 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)) } + for (const [x, r] of [[0.2 * A, 0.045], [0.86 * A, 0.038]]) groundShadow(this.hdr, W, H, this.shade, x * H, this.groundPx[clamp(Math.round(x * H), 0, W - 1)] + 0.03 * H, r * 6 * H, H) for (let i = 0; i < Math.round(A * 6); i++) { const x = (hash(i * 3.9) * A) * H + groundShadow(this.hdr, W, H, this.shade, x, this.groundPx[clamp(Math.round(x), 0, W - 1)] + 2, (0.1 + hash(i * 1.1) * 0.07) * H, 0.1 * 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) } // Big leaves framing the corners, drawn each frame so they can sway. diff --git a/wallpapers/prehistoric.ts b/wallpapers/prehistoric.ts index d37610a..0910eaf 100644 --- a/wallpapers/prehistoric.ts +++ b/wallpapers/prehistoric.ts @@ -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, 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" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -167,6 +168,7 @@ class Prehistoric extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private allStars: Star[] private stars: Star[] = [] @@ -193,6 +195,7 @@ class Prehistoric extends Canvas { this.activity = settings.activity this.look = LOOKS[settings.time] this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, HORIZON) + this.shade = sunShade(settings.time, this.look.orb, this.weather.covered) 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) if (settings.activity !== "teeming") return @@ -241,6 +244,7 @@ class Prehistoric extends Canvas { const [top, bottom] = this.weather.scud ?? [this.look.clouds.top, this.look.clouds.bottom] paintClouds(this.hdr, this.W, this.H, this.clouds, top, bottom, this.look.clouds.alpha, this.look.clouds.puffy) paintStorm(this.hdr, this.W, this.H, this.storm, this.look.clouds.top, this.look.clouds.bottom, this.gloom) + this.drawShadows() this.drawSmoke() if (this.activity !== "calm") { this.drawPterosaurs() @@ -324,6 +328,10 @@ class Prehistoric extends Canvas { }) if (this.activity === "teeming") this.paintLake() this.trees = [0.08 * A, Math.max(0.4 * A, 0.08 * A + 0.45)] + const ground = (x: number, y: number, w: number, h: number, tip?: number) => groundShadow(this.hdr, W, H, this.shade, x * H, y * H, w * H, h * H, tip) + for (const [x, base, top] of [[this.trees[0], 0.665, 0.29], [this.trees[1], 0.662, 0.31], [0.93 * A, 0.64, 0.45]]) ground(x, base, 0.08, base - top, 1.5) + for (const [x, y, s] of [[0.32, 0.78, 0.07], [0.88, 0.74, 0.08], [0.48, 0.9, 0.08]]) ground(x * A, y, s * 1.4, s * 0.8, 1.2) + for (const [x, y, s] of [[0.04, 0.86, 0.09], [0.47, 0.835, 0.07], [0.9, 0.84, 0.1]]) ground(x * A, y, s * 1.4, s * 1.1, 1.5) this.drawTree(this.trees[0], 0.665, 0.29) this.drawTree(this.trees[1], 0.662, 0.31) this.drawTree(0.93 * A, 0.64, 0.45) @@ -339,6 +347,25 @@ class Prehistoric extends Canvas { this.background = this.hdr.slice() } + // Shadows under everything that walks or drives, drawn before any of it so none falls across another animal. + private drawShadows() { + const H = this.H + const ground = (x: number, y: number, w: number, h: number, tip?: number) => groundShadow(this.hdr, this.W, H, this.shade, x * H, y * H, w * H, h * H, tip) + if (this.activity !== "calm") { + const s = this.sauropod + const [from, to] = this.sauropodSpots() + ground(lerp(from, to, s.p) - s.face * 0.06, 0.668, 0.28, 0.13, 0.8) + } + for (const w of this.herd) { + const S = 0.2 * (0.6 + (w.y - 0.7) * 2.5) * w.size + ground(w.x + w.face * 0.04 * S, w.y, 1.0 * S, 0.55 * S, 0.8) + } + const r = this.rex + if (r.x > -5) ground(r.x - r.dir * 0.03, 0.935, 0.24, 0.24, 0.8) + const d = this.delorean + if (d.t >= 0 && d.t <= DELOREAN) ground(d.x0 + 0.05 * Math.min(d.t, DELOREAN), 0.958, 0.18, 0.065, 0.9) + } + private paint(c: RGB): RGB { const t = this.look.tint return [c[0] * t[0], c[1] * t[1], c[2] * t[2]] diff --git a/wallpapers/tundra.ts b/wallpapers/tundra.ts index f7d1dd3..2d9a6c3 100644 --- a/wallpapers/tundra.ts +++ b/wallpapers/tundra.ts @@ -1,6 +1,7 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" import { eggWait } from "../src/egg" import { TAU, clamp, fbm1, hash, 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" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -133,6 +134,7 @@ class Tundra extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private stars: Star[] private clouds: Cloud[] @@ -156,6 +158,7 @@ class Tundra extends Canvas { 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. this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, HORIZON) + this.shade = sunShade(settings.time, this.look.orb, this.weather.covered) this.flakes = Array.from({ length: settings.weather ? 0 : { calm: 70, lively: 100, teeming: 130 }[settings.activity] }, () => ({ x: Math.random() * 4, y: Math.random(), z: Math.random(), s: Math.random() })) const walker = (x: number, y: number): Walker => ({ x, y, wx: x, wy: y, wanderT: 0, face: 1, phase: Math.random() * TAU, rest: 0, moving: false, hop: Infinity }) if (settings.activity !== "calm") { @@ -226,6 +229,7 @@ class Tundra extends Canvas { const [top, bottom] = this.weather.scud ?? [this.look.clouds.top, this.look.clouds.bottom] paintClouds(this.hdr, this.W, this.H, this.clouds, top, bottom, this.look.clouds.alpha, this.look.clouds.puffy) paintStorm(this.hdr, this.W, this.H, this.storm, this.look.clouds.top, this.look.clouds.bottom, this.gloom) + this.drawShadows() if (this.owl.x > -5) this.drawOwl() if (this.yeti.t >= 0) this.drawYeti() if (this.bear) this.drawBear(this.bear) @@ -261,6 +265,12 @@ class Tundra extends Canvas { 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)) } this.drawLake(0.5 * A, 0.72, 0.3, 0.045) + const ground = (x: number, y: number, w: number, h: number, tip?: number) => groundShadow(this.hdr, W, H, this.shade, x * H, y * H, w * H, h * H, tip) + for (const [fx, h] of [[0.3, 0.1], [0.36, 0.08], [0.7, 0.09], [0.76, 0.11]] as const) ground(fx * A, 0.625, h * 0.45, h, 0.5) + ground(0.86 * A, 0.7, 0.11, 0.047, 0.8) + ground(0.2 * A, 0.75, 0.15, 0.064, 0.8) + if (this.activity === "teeming") ground(0.3 * A, 0.85, 0.068, 0.15, 0.6) + ground(0.62 * A, 0.87, 0.09, 0.195, 0.6) 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) this.drawIgloo(0.2 * A, 0.75, 0.075) @@ -270,6 +280,27 @@ class Tundra extends Canvas { this.background = this.hdr.slice() } + // Shadows under everything on the snow, drawn before any of it so none falls across another animal. The yeti has + // none: it is never quite there. + private drawShadows() { + const H = this.H + const ground = (x: number, y: number, w: number, h: number, tip?: number, lift?: number) => groundShadow(this.hdr, this.W, H, this.shade, x * H, y * H, w * H, h * H, tip, lift) + if (this.bear) { + const S = 0.13 * this.depthScale(this.bear.y) + ground(this.bear.x + this.bear.face * 0.05 * S, this.bear.y, 1.0 * S, 0.6 * S, 0.8) + } + if (this.activity === "teeming") ground(0.68 * this.A + 0.05, 0.718, 0.04, 0.11, 0.6) + for (const p of this.penguins) { + const S = 0.07 * this.depthScale(p.y) + const hop = p.hop >= 0 && p.hop < HOP ? Math.abs(Math.sin((p.hop / HOP) * TAU)) * 0.025 * this.depthScale(p.y) : 0 + ground(p.x, p.y, 0.45 * S, S, 0.6, hop * H) + } + if (this.fox) { + const S = 0.08 * this.depthScale(this.fox.y) + ground(this.fox.x - this.fox.face * 0.05 * S, this.fox.y, 1.1 * S, 0.7 * S, 0.7) + } + } + private paint(c: RGB): RGB { const t = this.look.tint return [c[0] * t[0], c[1] * t[1], c[2] * t[2]] diff --git a/wallpapers/zen.ts b/wallpapers/zen.ts index c1f2340..b391c42 100644 --- a/wallpapers/zen.ts +++ b/wallpapers/zen.ts @@ -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, 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" import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper" import { WeatherLayer } from "../src/weather" @@ -196,6 +197,7 @@ class Zen extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private stars: Star[] private clouds: Cloud[] @@ -236,6 +238,7 @@ class Zen extends Canvas { this.look = LOOKS[settings.time] this.season = settings.season ?? "spring" this.weather = new WeatherLayer(settings.weather ?? (this.season === "winter" ? "snow" : "clear"), settings.time, HORIZON, !settings.weather) + this.shade = sunShade(settings.time, this.look.orb, this.weather.covered) this.stars = makeStars(this.look.stars, 0.45) this.clouds = makeClouds(this.look.clouds.puffy ? 3 : 5, this.look.clouds.puffy, 0.06, 0.26) const count = { calm: 0, lively: 3, teeming: 6 }[settings.activity] @@ -346,6 +349,12 @@ class Zen extends Canvas { this.drawHills() const sky = this.hdr.slice() this.drawGround() + // Shadows of the trees, rocks and lanterns on the gravel; the pond's water is redrawn over any that reach it. + const ground = (x: number, y: number, w: number, h: number, tip?: number) => groundShadow(this.hdr, W, H, this.shade, x * H, y * H, w * H, h * H, tip) + ground(0.07 * A, 0.66, 0.1, 0.4, 1.6) + ground(this.cherry.x + 0.08, 0.66, 0.12, 0.38, 2) + for (const r of this.rocks()) ground(r.x + r.r * 0.2, r.y + r.r * 0.15, r.r * 2.2, r.r * 1.4, 0.8) + for (const [i, l] of this.lanterns().entries()) ground(l.x, l.base, l.scale * (i === 0 ? 0.45 : 1), l.scale * (i === 0 ? 1 : 0.8), i === 0 ? 0.9 : 0.8) 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 @@ -1060,6 +1069,7 @@ class Zen extends Canvas { const sitting = t >= TANUKI.arrive && t < TANUKI.leave const u0 = t < TANUKI.arrive ? lerp(-0.12, seat, t / TANUKI.arrive) : sitting ? seat : lerp(seat, A + 0.12, (t - TANUKI.leave) / (TANUKI.gone - TANUKI.leave)) const S = 0.12 * H + groundShadow(this.hdr, this.W, H, this.shade, u0 * H, 0.965 * H, (sitting ? 0.4 : 0.65) * S, (sitting ? 0.6 : 0.4) * S) const P = (u: number, v: number): [number, number] => [u0 * H + u * S, 0.965 * H + v * S] const fur = this.paint([0.5, 0.4, 0.28]) const dark = this.paint([0.14, 0.11, 0.09]) @@ -1160,6 +1170,7 @@ class Zen extends Canvas { const S = 0.04 * H const x = pad.x * H const y = (pad.y + 0.003) * H + groundShadow(this.hdr, this.W, H, this.shade, x, y, 0.6 * S, 0.6 * S) const P = (u: number, v: number): [number, number] => [x + u * S, y + v * S] const green = this.paint([0.3, 0.58, 0.15]) const sac = this.frog.croak >= 0 ? Math.max(0, Math.sin(this.frog.croak * Math.PI * 2)) : 0 From 4ab55630143c6390971dab789e4abb939e3b7082 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 21:01:23 -0700 Subject: [PATCH 2/3] Document shadows and show them in the template --- CONTRIBUTING.md | 9 +++++++++ wallpapers/template.ts | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c067710..dc04ab0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,15 @@ 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`. +### Shadows + +`src/shadow.ts` grounds things standing on the ground. Make one `Shade` with `sunShade(time, look.orb, +weather.covered)`, then call `groundShadow(hdr, W, H, shade, x, y, w, h, tip?, lift?)` with the ground point under a +thing, its width and height in pixels, before drawing it: static things in `layout()`, moving ones each frame. It draws a +soft contact shadow and a cast shadow away from the light: short by day, long and spreading from the low sun at sunset, +faint at night, contact only under cloud. `lift` (pixels off the ground) fades the contact shadow and slides the cast one +away, for hops and lifts. Shadows multiply the ground toward a sky-tinted color, so they never gray it. + `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/wallpapers/template.ts b/wallpapers/template.ts index 9e3aa7e..ab87920 100644 --- a/wallpapers/template.ts +++ b/wallpapers/template.ts @@ -1,7 +1,7 @@ // A starter wallpaper: rolling hills with a tree, a hot-air balloon and birds. It is not registered in index.ts; try it // with `WALLPAPER_MODULES=$PWD/wallpapers/template.ts bun dev/snap.ts template teeming day 30`. In a package of your // own, import from "opencode-wallpapers/api" instead of "../src/api". See CONTRIBUTING.md. -import { Canvas, TAU, cap, clamp, driftClouds, ell, fbm1, makeClouds, makeStars, paintClouds, paintSky, paintStars, type Activity, type Cloud, type Lighting, type Orb, type RGB, type Settings, type Star, type Time, type Wallpaper } from "../src/api" +import { Canvas, TAU, cap, clamp, driftClouds, ell, fbm1, groundShadow, makeClouds, makeStars, paintClouds, paintSky, paintStars, sunShade, type Activity, type Cloud, type Lighting, type Orb, type RGB, type Settings, type Shade, type Star, type Time, type Wallpaper } from "../src/api" // Every wallpaper runs slower than real time, and its creatures slower still, so nothing is busy behind text. const TIME_SCALE = 0.35 @@ -90,6 +90,7 @@ class Template extends Canvas { private readonly activity: Activity private readonly look: Look private lighting: Lighting = { style: "front", dir: DAYLIGHT } + private readonly shade: Shade private background = new Float32Array(0) private stars: Star[] private clouds: Cloud[] @@ -99,6 +100,7 @@ class Template extends Canvas { super() this.activity = settings.activity this.look = LOOKS[settings.time] + this.shade = sunShade(settings.time, this.look.orb) this.stars = makeStars(this.look.stars, 0.55) this.clouds = makeClouds(4, true, 0.08, 0.3) } @@ -145,6 +147,8 @@ class Template extends Canvas { } // A tree on the near hill: shape() joins the parts into one lit form. const [tx, ty] = [0.8 * A * H, 0.86 * H] + // Its shadow first, so the tree stands on it: width and height in pixels, and how much wider the crown's end is. + groundShadow(hdr, W, H, this.shade, tx, ty, 0.08 * H, 0.24 * H, 1.6) this.shape([cap(tx, ty, tx, ty - 0.12 * H, 0.012 * H, 0.008 * H, this.paint([0.4, 0.28, 0.18]))], this.lighting) const leaves = this.paint([0.2, 0.42, 0.14]) this.shape([ell(tx, ty - 0.18 * H, 0.07 * H, 0.06 * H, 0, leaves), ell(tx - 0.05 * H, ty - 0.14 * H, 0.045 * H, 0.04 * H, 0, leaves), ell(tx + 0.05 * H, ty - 0.14 * H, 0.045 * H, 0.04 * H, 0, leaves)], this.lighting) From 7a7c5f0a54523fa5f3f041c6b84dadc0513fb5ea Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 21:06:45 -0700 Subject: [PATCH 3/3] Drop the unused shade tint override --- src/shadow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shadow.ts b/src/shadow.ts index 83ae2dd..1627f05 100644 --- a/src/shadow.ts +++ b/src/shadow.ts @@ -26,7 +26,7 @@ export interface Shade { // Short, soft shadows by day; long ones at sunset, spreading away from the low sun; at night faint contact shadows and // a hint of moon shadow. Under cloud the light is diffuse, so only contact shadows remain. orb is the sun or moon, in // fractions of the width and height. -export function sunShade(time: Time, orb: { x: number; y: number }, covered = false, tint?: RGB): Shade { +export function sunShade(time: Time, orb: { x: number; y: number }, covered = false): Shade { const away = orb.x < 0.5 ? 1 : -1 const looks: Record = { day: { tint: [0.36, 0.44, 0.64], contact: 0.75, cast: 0.6, length: 0.6, x: away, y: 0.45, radiate: false }, @@ -34,7 +34,7 @@ export function sunShade(time: Time, orb: { x: number; y: number }, covered = fa night: { tint: [0.35, 0.45, 0.85], contact: 0.45, cast: 0.2, length: 0.6, x: away, y: 0.45, radiate: false }, } const shade = looks[time] - return { ...shade, tint: tint ?? shade.tint, contact: covered ? shade.contact * 0.8 : shade.contact, cast: covered ? 0 : shade.cast } + return { ...shade, contact: covered ? shade.contact * 0.8 : shade.contact, cast: covered ? 0 : shade.cast } } // The shadows of a thing w pixels wide and h tall standing at (x, y), the ground under its middle. tip is the width