From f8b21385273ece8fabfcd42faa5882fae2cea08f Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:36:52 -0700 Subject: [PATCH 01/10] Give farm animals and the farmer jointed legs, real walk cycles and turns --- src/creature.ts | 119 ++++++++++++++++++++++++ wallpapers/farm.ts | 220 ++++++++++++++++++++++++++++----------------- 2 files changed, 259 insertions(+), 80 deletions(-) create mode 100644 src/creature.ts diff --git a/src/creature.ts b/src/creature.ts new file mode 100644 index 0000000..c9827f1 --- /dev/null +++ b/src/creature.ts @@ -0,0 +1,119 @@ +// Walking and follow-through for creatures drawn with Canvas.shape. Creatures are posed in their own body coordinates +// (u forward, v down, w toward their right side) and mapped to pixels by a view, so legs, joints and turns are worked +// out once here instead of in every scene. +import { cap, type Part } from "./canvas" +import { TAU, clamp, smoothstep, type RGB } from "./math" + +// A walking creature's animation state. phase counts strides: advance it by distance walked over stride length and +// planted feet stay put on the ground. go eases between standing (0) and walking (1), turn eases facing from 1 (right) +// to -1 (left) through a three-quarter view, and lag is a springy follow-through that overshoots as the creature +// starts, stops and turns, for tails, ears, manes and heads to trail by. +export interface Gait { + phase: number + go: number + turn: number + lag: number + lagV: number +} + +export function gait(face = 1, phase = 0): Gait { + return { phase, go: 0, turn: face, lag: 0, lagV: 0 } +} + +// Moves a gait on by `strides` (distance over stride length) in dt creature seconds, turning toward face (1 or -1) at +// `rate` (2 swings it right round in a creature second). +export function stepGait(g: Gait, strides: number, dt: number, face: number, rate = 3) { + g.phase += strides + g.go = clamp(g.go + (strides > 0 ? dt : -dt) * 2.5, 0, 1) + const before = g.turn + g.turn = face > g.turn ? Math.min(face, g.turn + dt * rate) : Math.max(face, g.turn - dt * rate) + // The spring chases the walking pace and is kicked by turning, then rings down. + const target = g.go + Math.abs(g.turn - before) / Math.max(dt, 1e-6) * 0.15 + g.lagV += ((target - g.lag) * 40 - g.lagV * 5) * dt + g.lag += g.lagV * dt +} + +// How a creature is seen at turn t: along scales its length (1 side-on facing right, -1 facing left) and across its +// width, which shows as it turns to face the viewer on the way round. +export function view(turn: number): [along: number, across: number] { + const a = ((1 - turn) * Math.PI) / 2 + return [Math.cos(a), Math.sin(a)] +} + +// One foot's place in the stride: how far it reaches ahead of its hip (1 at touchdown, -1 at lift-off) and how high it +// is lifted (0 on the ground). It is planted for the first `duty` of each cycle, sliding back exactly as fast as the +// body moves on, then swings forward. A creature whose stride (distance per cycle) is d keeps feet planted with a +// reach of d * duty / 2. +export function stride(phase: number, offset = 0, duty = 0.6): [reach: number, lift: number] { + const p = (((phase + offset) % 1) + 1) % 1 + if (p < duty) return [1 - (2 * p) / duty, 0] + const q = (p - duty) / (1 - duty) + return [-1 + 2 * smoothstep(0, 1, q), Math.sin(Math.PI * q)] +} + +// How high the body rides: 1 over each planted leg, 0 between, twice a stride. +export function bob(phase: number, duty = 0.6) { + return 0.5 + 0.5 * Math.cos(TAU * 2 * (phase - duty / 2)) +} + +// The middle joint of a two-segment limb of lengths a and b from (ax, ay) to (bx, by). bend 1 puts it on the side of +// the line's left normal, behind a leg hanging from a creature facing +x (a hock or a bird's ankle); -1 in front (a +// knee or elbow). Out of reach, the limb straightens toward the tip. +export function joint(ax: number, ay: number, bx: number, by: number, a: number, b: number, bend: number): [number, number] { + const dx = bx - ax + const dy = by - ay + const d = Math.hypot(dx, dy) || 1e-6 + const r = clamp(d, Math.abs(a - b) + 1e-6, a + b - 1e-6) + const along = (a * a - b * b + r * r) / (2 * r) + const h = Math.sqrt(Math.max(0, a * a - along * along)) * bend + return [ax + (dx / d) * along - (dy / d) * h, ay + (dy / d) * along + (dx / d) * h] +} + +// A tapered two-segment limb in body coordinates, mapped to pixels by P: root radius r0, joint r1, tip r2. +export function limb(P: (u: number, v: number) => [number, number], ax: number, ay: number, bx: number, by: number, a: number, b: number, bend: number, r0: number, r1: number, r2: number, color: RGB): Part[] { + const [jx, jy] = joint(ax, ay, bx, by, a, b, bend) + return [cap(...P(ax, ay), ...P(jx, jy), r0, r1, color), cap(...P(jx, jy), ...P(bx, by), r1, r2, color)] +} + +// Body coordinates to pixels: u along the creature, v down, w across it toward its right side, all in units of S pixels. +// along and across are its view (see view), and len(u, w) the on-screen half-length in pixels of a part u long and w +// wide, for ellipses that foreshorten as the creature turns. +export type Body = ((u: number, v: number, w?: number) => [number, number]) & { S: number; along: number; across: number; len: (u: number, w: number) => number } + +// A creature's body coordinates seen at turn (see view): x, y is where it stands in pixels and S its size in pixels. +export function body(x: number, y: number, S: number, turn: number): Body { + const [along, across] = view(turn) + const P = (u: number, v: number, w = 0): [number, number] => [x + (u * along - w * across) * S, y + v * S] + return Object.assign(P, { S, along, across, len: (u: number, w: number) => (u * Math.abs(along) + w * across) * S }) +} + +// A quadruped's legs on its far or near side, walking in diagonal pairs. Hips sit `fore` ahead of and `hind` behind +// the middle, `top` above the ground and `w` either side; a and b are the upper and lower leg lengths in body units, +// reach and lift how far a foot travels (see stride), and r the leg's radii at the hip, joint and foot. Front knees +// bend forward and hocks back. paw adds a hoof or paw that long, in its own color. +export function quadruped(P: Body, g: Gait, far: boolean, o: { fore: number; hind: number; top: number; w: number; a: number; b: number; reach: number; lift: number; r: [number, number, number]; color: RGB; paw?: [number, RGB] }): Part[] { + const S = P.S + const [r0, r1, r2] = o.r + const along = view(g.turn)[0] + const legs: [number, number, number][] = [ + [o.fore, o.w, 0], + [-o.hind, -o.w, 0], + [o.fore, -o.w, 0.5], + [-o.hind, o.w, 0.5], + ] + return legs + .filter(([, w]) => w * along < 0 === far) + .flatMap(([u, w, offset]) => { + const [reach, lift] = stride(g.phase, offset) + const fu = u + reach * o.reach * g.go + const fv = -lift * o.lift * g.go - r2 + const Q = (a: number, b: number) => P(a, b, w) + const parts = limb(Q, u, -o.top, fu, fv, o.a, o.b, u > 0 ? -1 : 1, r0 * S, r1 * S, r2 * S, o.color) + return o.paw ? [...parts, cap(...Q(fu, fv), ...Q(fu + o.paw[0], fv + r2 * 0.1), r2 * 1.15 * S, r2 * S, o.paw[1])] : parts + }) +} + +// A chain of tapered capsules through points along a curve, for tails and necks: path(t) for t 0..1 in pixels. +export function chain(path: (t: number) => [number, number], n: number, r0: number, r1: number, color: RGB): Part[] { + return Array.from({ length: n }, (_, i) => cap(...path(i / n), ...path((i + 1) / n), r0 + (r1 - r0) * (i / n), r0 + (r1 - r0) * ((i + 1) / n), color)) +} diff --git a/wallpapers/farm.ts b/wallpapers/farm.ts index 868c944..5d4af0a 100644 --- a/wallpapers/farm.ts +++ b/wallpapers/farm.ts @@ -1,4 +1,5 @@ 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 { 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" @@ -161,6 +162,7 @@ interface Walker { // Time left standing with the head up, looking toward a click. look: number moving: boolean + g: Gait } class Farm extends Canvas { @@ -177,7 +179,7 @@ class Farm extends Canvas { private cows: Walker[] = [] private chickens: Walker[] = [] private pigs: Walker[] = [] - private farmer = { x: 0.5, dir: 1, phase: 0, wave: -1, next: 20 } + private farmer = { x: 0.5, dir: 1, wave: -1, next: 20, g: gait() } private tractor = { x: -9, dir: 1, next: 14, wheel: 0 } private smoke: { x: number; y: number; age: number }[] = [] private fireflies: { x: number; y: number; vx: number; vy: number; phase: number }[] = [] @@ -204,7 +206,7 @@ class Farm extends Canvas { 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] })) 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) - const walker = (x: number, y: number): Walker => ({ x, y, wx: x, wy: y, wanderT: 0, face: 1, phase: Math.random() * TAU, graze: 0, rest: 0, look: 0, moving: false }) + const walker = (x: number, y: number): Walker => ({ x, y, wx: x, wy: y, wanderT: 0, face: 1, phase: Math.random() * TAU, graze: 0, rest: 0, look: 0, moving: false, g: gait(1, hash(x * 7 + y)) }) if (settings.activity !== "calm") { this.cows = [walker(0.9, 0.8), walker(1.3, 0.84), ...(settings.activity === "teeming" ? [walker(1.1, 0.78)] : [])] this.chickens = [walker(1.2, 0.69), walker(1.3, 0.7), walker(1.4, 0.685)] @@ -243,9 +245,9 @@ class Farm extends Canvas { } this.stepTractor(dt) const A = this.A - for (const c of this.cows) this.wander(c, cdt, [0.4 * A, 0.95 * A, 0.76, 0.87], 0.03, [6, 14]) - for (const c of this.chickens) this.wander(c, cdt, [0.62 * A, 0.84 * A, 0.672, 0.712], 0.025, [2, 5]) - for (const p of this.pigs) this.wander(p, cdt, [0.44 * A, 0.6 * A, 0.88, 0.94], 0.02, [5, 10]) + for (const c of this.cows) this.wander(c, cdt, [0.4 * A, 0.95 * A, 0.76, 0.87], 0.03, [6, 14], 0.055) + for (const c of this.chickens) this.wander(c, cdt, [0.62 * A, 0.84 * A, 0.672, 0.712], 0.025, [2, 5], 0.025) + for (const p of this.pigs) this.wander(p, cdt, [0.44 * A, 0.6 * A, 0.88, 0.94], 0.02, [5, 10], 0.035) if (this.activity === "teeming") this.stepFarmer(cdt) this.stepEgg(dt, cdt) } @@ -551,8 +553,15 @@ class Farm extends Canvas { } } - // Walks between random points in a zone, pausing at each to graze or peck. - private wander(w: Walker, dt: number, zone: [number, number, number, number], speed: number, pause: [number, number]) { + // Walks between random points in a zone, pausing at each to graze or peck. stride is the distance a full step cycle + // covers near the front of the field, so the legs keep pace with the ground. + private wander(w: Walker, dt: number, zone: [number, number, number, number], speed: number, pause: [number, number], stride: number) { + const [x0, y0] = [w.x, w.y] + this.roam(w, dt, zone, speed, pause) + stepGait(w.g, Math.hypot(w.x - x0, w.y - y0) / (stride * this.depthScale(w.y)), dt, w.face) + } + + private roam(w: Walker, dt: number, zone: [number, number, number, number], speed: number, pause: [number, number]) { w.phase += dt * (w.moving ? 6 : 1.5) if (w.look > 0) { w.look -= dt @@ -592,92 +601,129 @@ class Farm extends Canvas { return 0.6 + (y - 0.7) * 2.5 } - // A black-and-white dairy cow seen from the side, lowering its head to graze. lift raises it off the ground and - // size shrinks it, for the saucer's beam. + // A black-and-white dairy cow on jointed legs, walking in diagonal pairs with its head nodding and its tail swinging + // behind, and lowering its head to graze. lift raises it off the ground and size shrinks it, for the saucer's beam. private drawCow(c: Walker, lift = 0, size = 1) { const H = this.H - const S = 0.11 * H * this.depthScale(c.y) * size - const x = c.x * H - const y = (c.y - lift) * H - const P = (u: number, v: number): [number, number] => [x + u * c.face * S, y + v * S] - const swing = c.moving ? Math.sin(c.phase) * 0.05 : 0 + const g = c.g + const P = body(c.x * H, (c.y - lift) * H, 0.11 * H * this.depthScale(c.y) * size, g.turn) + const S = P.S const hide = this.paint([0.92, 0.9, 0.86]) - const g = c.graze - const [hu, hv] = [lerp(0.55, 0.6, g), lerp(-0.56, -0.2, g)] - const parts: Part[] = [] - for (const [u, s] of [[0.28, 1], [0.34, -1], [-0.24, -1], [-0.3, 1]] as const) parts.push(cap(...P(u, -0.3), ...P(u + swing * s, 0), 0.045 * S, 0.038 * S, hide)) - parts.push( - cap(...P(-0.4, -0.5), ...P(-0.47, -0.2), 0.014 * S, 0.012 * S, hide), - ell(...P(0, -0.43), 0.42 * S, 0.2 * S, 0, hide), - ell(...P(-0.1, -0.25), 0.07 * S, 0.045 * S, 0, this.paint([0.9, 0.6, 0.6])), - cap(...P(0.33, -0.5), ...P(hu - 0.06, hv), 0.11 * S, 0.08 * S, hide), - ell(...P(hu, hv), 0.13 * S, 0.09 * S, lerp(-0.25, 1, g) * c.face, hide), - ell(...P(hu + lerp(0.1, 0.03, g), hv + lerp(0.03, 0.1, g)), 0.06 * S, 0.055 * S, 0, this.paint([0.88, 0.62, 0.58])), - cap(...P(hu - 0.05, hv - 0.07), ...P(hu - 0.14, hv - 0.09), 0.025 * S, 0.012 * S, hide), - ell(...P(-0.47, -0.17), 0.025 * S, 0.035 * S, 0, this.paint(BLACK)), + const black = this.paint(BLACK) + const rise = bob(g.phase) * g.go * 0.015 + const legs = { fore: 0.25, hind: 0.27, top: 0.42 - rise, w: 0.08, a: 0.21, b: 0.21, reach: 0.15, lift: 0.08, r: [0.07, 0.036, 0.03] as [number, number, number], paw: [0.03, this.paint([0.25, 0.2, 0.17])] as [number, RGB] } + this.shape(quadruped(P, g, true, { ...legs, color: this.paint([0.66, 0.64, 0.6]) }), this.lighting, 0.7) + const gz = smoothstep(0, 1, c.graze) + const nod = Math.cos(TAU * 2 * g.phase - 1) * 0.02 * g.go + const poll: [number, number] = [lerp(0.47, 0.54, gz), lerp(-0.6, -0.26, gz) + nod - rise] + const muzzle: [number, number] = [poll[0] + lerp(0.15, 0.05, gz), poll[1] + lerp(0.11, 0.2, gz)] + const swish = Math.sin(this.creatureTime * 2.3 + c.x * 17) * 0.05 - g.lag * 0.06 + const tail = (t: number) => P(-0.43 - t * 0.03 + swish * t * t, -0.6 - rise + t * 0.36) + this.shape( + [ + ...quadruped(P, g, false, { ...legs, color: hide }), + ell(...P(0.01, -0.47 - rise), P.len(0.34, 0.15), 0.17 * S, 0, hide), + ell(...P(-0.25, -0.5 - rise), P.len(0.16, 0.14), 0.15 * S, 0, hide), + ell(...P(0.24, -0.47 - rise), P.len(0.15, 0.14), 0.16 * S, 0, hide), + ell(...P(-0.12, -0.31 - rise), P.len(0.07, 0.05), 0.045 * S, 0, this.paint([0.9, 0.6, 0.6])), + cap(...P(0.28, -0.5 - rise), ...P(...poll), 0.13 * S, 0.085 * S, hide), + cap(...P(...poll), ...P(...muzzle), 0.085 * S, 0.068 * S, hide), + ell(...P(...muzzle), 0.06 * S, 0.055 * S, 0, this.paint([0.88, 0.62, 0.58])), + ...[-1, 1].map((s) => ell(...P(poll[0] - 0.04, poll[1] - 0.01, s * 0.1), 0.075 * S, 0.026 * S, (-0.2 - g.lag * 0.15) * P.along, hide)), + ...chain(tail, 3, 0.02 * S, 0.012 * S, hide), + ell(...tail(1.1), 0.025 * S, 0.045 * S, 0, black), + ], + this.lighting, + 0.7, + ) + this.shape( + [ + ell(...P(-0.15, -0.5 - rise), P.len(0.12, 0.06), 0.08 * S, 0.3 * P.along, black), + ell(...P(0.13, -0.4 - rise), P.len(0.1, 0.05), 0.07 * S, -0.4 * P.along, black), + ell(...P(-0.3, -0.38 - rise), P.len(0.06, 0.03), 0.05 * S, 0, black), + ell(...P(poll[0] + 0.02, poll[1] + 0.02), 0.05 * S, 0.045 * S, 0, black), + ], + this.lighting, + 0.4, ) - this.shape(parts, this.lighting, 0.7) - const spots = this.paint(BLACK) - this.shape([ell(...P(-0.15, -0.48), 0.12 * S, 0.08 * S, 0.3, spots), ell(...P(0.13, -0.39), 0.1 * S, 0.07 * S, -0.4, spots), ell(...P(-0.3, -0.37), 0.06 * S, 0.05 * S, 0, spots), ell(...P(hu - 0.02, hv - 0.02), 0.05 * S, 0.04 * S, 0, spots)], this.lighting, 0.4) } - // A hen bobbing its head to peck at the ground. + // A hen high-stepping on jointed legs, her head holding still in the air and darting forward at each step, and + // pecking at the ground when she stops. private drawChicken(h: Walker) { const H = this.H - const S = 0.05 * H * this.depthScale(h.y) - const x = h.x * H - const y = h.y * H - const P = (u: number, v: number): [number, number] => [x + u * h.face * S, y + v * S] - const peck = Math.max(h.graze, Math.pow(Math.max(0, Math.sin(h.phase * 2)), 6)) + const g = h.g + const P = body(h.x * H, h.y * H, 0.05 * H * this.depthScale(h.y), g.turn) + const S = P.S + const peck = Math.max(h.graze, Math.pow(Math.max(0, Math.sin(h.phase * 2)), 6) * (1 - g.go)) const feather = this.paint(h.x % 0.3 > 0.15 ? [0.6, 0.35, 0.15] : [0.92, 0.9, 0.82]) const leg = this.paint([0.95, 0.6, 0.15]) - const [hu, hv] = [lerp(0.32, 0.45, peck), lerp(-0.78, -0.35, peck)] + const red = this.paint([0.85, 0.08, 0.06]) + const rise = bob(g.phase) * g.go * 0.03 + const f = (g.phase * 2) % 1 + const thrust = g.go * 0.09 * (f < 0.75 ? 1 - (2 * f) / 0.75 : -1 + 2 * smoothstep(0.75, 1, f)) + const [hu, hv] = [lerp(0.3, 0.48, peck) + thrust, lerp(-0.84, -0.3, peck) - rise] + const legs = [-1, 1].flatMap((w) => { + const [reach, raise] = stride(g.phase, w > 0 ? 0 : 0.5) + const fu = reach * 0.15 * g.go + const fv = -raise * 0.12 * g.go - 0.02 + const Q = (u: number, v: number) => P(u, v, w * 0.06) + return [...limb(Q, 0, -0.4 - rise, fu, fv, 0.2, 0.22, 1, 0.07 * S, 0.03 * S, 0.025 * S, leg), cap(...Q(fu - 0.06, fv), ...Q(fu + 0.1, fv + 0.01), 0.025 * S, 0.015 * S, leg)] + }) this.shape( [ - cap(...P(0.05, -0.25), ...P(0.05, 0), 0.03 * S, 0.025 * S, leg), - cap(...P(-0.08, -0.25), ...P(-0.08, 0), 0.03 * S, 0.025 * S, leg), - cap(...P(-0.25, -0.5), ...P(-0.45, -0.85), 0.14 * S, 0.06 * S, feather), - ell(...P(0, -0.48), 0.35 * S, 0.27 * S, 0, feather), - cap(...P(0.2, -0.55), ...P(hu, hv), 0.12 * S, 0.1 * S, feather), - ell(...P(hu, hv), 0.14 * S, 0.13 * S, 0, feather), - ell(...P(hu, hv - 0.15), 0.06 * S, 0.06 * S, 0, this.paint([0.85, 0.08, 0.06])), - cap(...P(hu + 0.1, hv), ...P(hu + 0.24, hv + 0.03), 0.04 * S, 0.01 * S, leg), + ...legs, + cap(...P(-0.2, -0.55 - rise), ...P(-0.36 - g.lag * 0.03, -0.88 - rise), 0.13 * S, 0.05 * S, feather), + cap(...P(-0.2, -0.52 - rise), ...P(-0.42 - g.lag * 0.04, -0.72 - rise), 0.1 * S, 0.04 * S, feather), + ell(...P(0, -0.52 - rise), P.len(0.3, 0.18), 0.21 * S, 0.25 * P.along, feather), + ell(...P(0.14, -0.55 - rise), P.len(0.17, 0.16), 0.18 * S, 0, feather), + cap(...P(0.16, -0.62 - rise), ...P(hu, hv), 0.1 * S, 0.075 * S, feather), + ell(...P(hu, hv), 0.1 * S, 0.1 * S, 0, feather), + ell(...P(hu - 0.01, hv - 0.1), P.len(0.07, 0.02), 0.045 * S, 0, red), + ell(...P(hu + 0.05, hv + 0.09), 0.03 * S, 0.045 * S, 0, red), + cap(...P(hu + 0.07, hv), ...P(hu + 0.18, hv + 0.03), 0.035 * S, 0.008 * S, leg), ], this.lighting, 0.6, ) } - // A round pink pig, rooting in the mud. + // A round pink pig trotting on short legs, floppy ears bouncing and curly tail up, rooting in the mud. private drawPig(p: Walker) { const H = this.H - const S = 0.08 * H * this.depthScale(p.y) - const x = p.x * H - const y = p.y * H - const P = (u: number, v: number): [number, number] => [x + u * p.face * S, y + v * S] + const g = p.g + const P = body(p.x * H, p.y * H, 0.08 * H * this.depthScale(p.y), g.turn) + const S = P.S const pink = this.paint([0.95, 0.66, 0.66]) - const swing = p.moving ? Math.sin(p.phase) * 0.04 : 0 - const dip = p.graze * 0.12 + const snout = this.paint([0.88, 0.5, 0.52]) + const rise = bob(g.phase) * g.go * 0.015 + const legs = { fore: 0.2, hind: 0.21, top: 0.22 - rise, w: 0.1, a: 0.1, b: 0.1, reach: 0.13, lift: 0.06, r: [0.07, 0.045, 0.04] as [number, number, number], paw: [0.025, this.paint([0.5, 0.32, 0.3])] as [number, RGB] } + this.shape(quadruped(P, g, true, { ...legs, color: this.paint([0.75, 0.5, 0.5]) }), this.lighting, 0.7) + const dip = p.graze * 0.14 + Math.cos(TAU * 2 * g.phase - 1) * 0.015 * g.go - rise + const flop = g.lag * 0.03 + Math.sin(TAU * 2 * g.phase - 2) * 0.015 * g.go + const curl = (t: number) => P(-0.4 - 0.05 * Math.sin(t * 5), -0.44 - rise + 0.05 * Math.cos(t * 5) - 0.03 * t) this.shape( [ - ...[[0.22, 1], [0.28, -1], [-0.2, -1], [-0.26, 1]].map(([u, s]) => cap(...P(u, -0.15), ...P(u + swing * s, 0), 0.05 * S, 0.045 * S, pink)), - ell(...P(0, -0.32), 0.38 * S, 0.24 * S, 0, pink), - ell(...P(0.36, -0.36 + dip), 0.15 * S, 0.14 * S, 0, pink), - ell(...P(0.5, -0.33 + dip), 0.06 * S, 0.07 * S, 0, this.paint([0.88, 0.5, 0.52])), - cap(...P(0.3, -0.46 + dip), ...P(0.38, -0.58 + dip), 0.05 * S, 0.01 * S, pink), - cap(...P(-0.36, -0.36), ...P(-0.44, -0.44), 0.02 * S, 0.015 * S, pink), + ...quadruped(P, g, false, { ...legs, color: pink }), + ell(...P(-0.04, -0.34 - rise), P.len(0.38, 0.2), 0.21 * S, -0.06 * P.along, pink), + ell(...P(0.34, -0.37 + dip), 0.15 * S, 0.15 * S, 0, pink), + cap(...P(0.4, -0.35 + dip), ...P(0.52, -0.31 + dip), 0.1 * S, 0.075 * S, pink), + ell(...P(0.54, -0.31 + dip), P.len(0.025, 0.065), 0.065 * S, 0, snout), + ...[-1, 1].map((s) => cap(...P(0.3, -0.46 + dip, s * 0.08), ...P(0.42, -0.4 + dip + flop, s * 0.11), 0.055 * S, 0.02 * S, pink)), + ...chain(curl, 3, 0.02 * S, 0.015 * S, pink), ], this.lighting, 0.7, ) } - // The farmer strolls along the fence in a straw hat, now and then stopping to wave. + // The farmer strolls along the fence in a straw hat, turning round at each end and now and then stopping to wave. private stepFarmer(dt: number) { const f = this.farmer if (f.wave >= 0) { f.wave += dt if (f.wave > 3) f.wave = -1 + stepGait(f.g, 0, dt, f.dir) return } f.next -= dt @@ -686,8 +732,8 @@ class Farm extends Canvas { f.next = rand(20, 35) return } - f.phase += dt * 5 f.x += f.dir * 0.03 * dt + stepGait(f.g, (0.03 * dt) / 0.06, dt, f.dir) if (f.x > 0.95 * this.A) f.dir = -1 if (f.x < 0.42 * this.A) f.dir = 1 } @@ -695,30 +741,43 @@ class Farm extends Canvas { private drawFarmer() { const H = this.H const f = this.farmer - const S = 0.15 * H - const x = f.x * H - const y = 0.738 * H - const P = (u: number, v: number): [number, number] => [x + u * f.dir * S, y + v * S] - const walking = f.wave < 0 - const swing = walking ? Math.sin(f.phase) * 0.12 : 0 + const g = f.g + const rise = bob(g.phase) * g.go * 0.015 + const P = body(f.x * H, 0.738 * H, 0.08 * H, g.turn) + const S = P.S const denim = this.paint([0.2, 0.3, 0.6]) const shirt = this.paint([0.75, 0.15, 0.12]) const skin = this.paint([0.85, 0.65, 0.5]) const straw = this.paint([0.9, 0.78, 0.4]) - const waveArm: [number, number] = f.wave >= 0 ? [0.14 + Math.sin(f.wave * 8) * 0.04, -1.02] : [swing * 0.6, -0.5] + const boot = this.paint([0.3, 0.2, 0.12]) + // Each side's leg and the opposite arm swing together; the near arm waves. + const side = (w: number, far: boolean): Part[] => { + const [reach, raise] = stride(g.phase, w > 0 ? 0 : 0.5) + const fu = reach * 0.24 * g.go + const fv = -raise * 0.07 * g.go - 0.03 + const Q = (u: number, v: number) => P(u, v, w * 0.05) + const A = (u: number, v: number) => P(u, v, w * 0.1) + const waving = f.wave >= 0 && !far + const hand: [number, number] = waving ? [0.12 + Math.sin(f.wave * 8) * 0.04, -1.1] : [-reach * 0.14 * g.go + 0.02, -0.42 - rise] + const pants = far ? this.paint([0.13, 0.2, 0.42]) : denim + return [ + ...limb(Q, 0, -0.48 - rise, fu, fv, 0.25, 0.25, -1, 0.055 * S, 0.045 * S, 0.04 * S, pants), + cap(...Q(fu - 0.02, fv), ...Q(fu + 0.06, fv + 0.005), 0.035 * S, 0.03 * S, boot), + ...limb(A, 0.01, -0.78 - rise, ...hand, 0.19, 0.19, waving ? -1 : 1, 0.04 * S, 0.033 * S, 0.03 * S, far ? this.paint([0.5, 0.1, 0.08]) : shirt), + ell(...A(...hand), 0.03 * S, 0.03 * S, 0, skin), + ] + } + const near = P.along >= 0 ? 1 : -1 + this.shape(side(-near, true), this.lighting, 0.7) this.shape( [ - cap(...P(0.01, -0.75), ...P(-swing * 0.6, -0.5), 0.035 * S, 0.03 * S, shirt), - cap(...P(0, -0.45), ...P(-swing, 0), 0.05 * S, 0.045 * S, denim), - ell(...P(-swing + 0.03, -0.01), 0.05 * S, 0.02 * S, 0, this.paint([0.3, 0.2, 0.12])), - cap(...P(0, -0.45), ...P(swing, 0), 0.05 * S, 0.045 * S, denim), - ell(...P(swing + 0.03, -0.01), 0.05 * S, 0.02 * S, 0, this.paint([0.3, 0.2, 0.12])), - cap(...P(0, -0.42), ...P(0.02, -0.78), 0.085 * S, 0.075 * S, denim), - cap(...P(0.02, -0.78), ...P(0.02, -0.66), 0.07 * S, 0.06 * S, shirt), - cap(...P(0.03, -0.75), ...P(...waveArm), 0.035 * S, 0.03 * S, shirt), - ell(...P(0.04, -0.87), 0.065 * S, 0.07 * S, 0, skin), - ell(...P(0.04, -0.93), 0.15 * S, 0.028 * S, 0, straw), - ell(...P(0.04, -0.97), 0.075 * S, 0.05 * S, 0, straw), + ...side(near, false), + cap(...P(0, -0.45 - rise), ...P(0.02, -0.78 - rise), 0.1 * S, 0.085 * S, denim), + cap(...P(0.02, -0.8 - rise, -0.06), ...P(0.02, -0.8 - rise, 0.06), 0.06 * S, 0.06 * S, shirt), + ell(...P(0.04, -0.89 - rise), 0.065 * S, 0.07 * S, 0, skin), + cap(...P(0.08, -0.89 - rise), ...P(0.115, -0.87 - rise), 0.02 * S, 0.015 * S, skin), + ell(...P(0.04, -0.95 - rise), 0.15 * S, 0.028 * S, 0, straw), + ell(...P(0.03, -0.99 - rise), 0.075 * S, 0.05 * S, 0, straw), ], this.lighting, 0.7, @@ -803,13 +862,13 @@ class Farm extends Canvas { if (!c) { e.wait -= dt if (e.wait > 0) return - e.cow = { x: this.A + 0.15, y: 0.83, wx: 0, wy: 0, wanderT: 0, face: -1, phase: 0, graze: 0, rest: 0, look: 0, moving: true } + e.cow = { x: this.A + 0.15, y: 0.83, wx: 0, wy: 0, wanderT: 0, face: -1, phase: 0, graze: 0, rest: 0, look: 0, moving: true, g: gait(-1) } e.t = -1 return } if (e.t < 0) { - c.phase += cdt * 6 c.x -= 0.03 * cdt + stepGait(c.g, (0.03 * cdt) / (0.055 * this.depthScale(c.y)), cdt, -1) if (c.x > 0.9 * this.A) return c.moving = false e.t = 0 @@ -817,6 +876,7 @@ class Farm extends Canvas { } e.t += dt c.phase += cdt * 1.5 + stepGait(c.g, 0, cdt, -1) // Grazing until the beam comes on, then head up in surprise. c.graze = e.t < 11 ? Math.min(1, c.graze + cdt * 0.8) : Math.max(0, c.graze - dt * 2) if (e.t < 36) return From bd40492b54ce75970d17ca2d9d92a913b442c22d Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:40:12 -0700 Subject: [PATCH 02/10] Walk the dinosaurs on jointed legs with swinging tails --- src/creature.ts | 6 +- wallpapers/prehistoric.ts | 172 ++++++++++++++++++++------------------ 2 files changed, 94 insertions(+), 84 deletions(-) diff --git a/src/creature.ts b/src/creature.ts index c9827f1..d7dee2e 100644 --- a/src/creature.ts +++ b/src/creature.ts @@ -88,12 +88,14 @@ export function body(x: number, y: number, S: number, turn: number): Body { } // A quadruped's legs on its far or near side, walking in diagonal pairs. Hips sit `fore` ahead of and `hind` behind -// the middle, `top` above the ground and `w` either side; a and b are the upper and lower leg lengths in body units, +// the middle, `top` above the ground and `w` either side; a and b are the proportions of the upper and lower leg, // reach and lift how far a foot travels (see stride), and r the leg's radii at the hip, joint and foot. Front knees // bend forward and hocks back. paw adds a hoof or paw that long, in its own color. export function quadruped(P: Body, g: Gait, far: boolean, o: { fore: number; hind: number; top: number; w: number; a: number; b: number; reach: number; lift: number; r: [number, number, number]; color: RGB; paw?: [number, RGB] }): Part[] { const S = P.S const [r0, r1, r2] = o.r + // Standing, legs are a little bent; a and b only set the proportions. + const k = ((o.top - r2) * 1.06) / (o.a + o.b) const along = view(g.turn)[0] const legs: [number, number, number][] = [ [o.fore, o.w, 0], @@ -108,7 +110,7 @@ export function quadruped(P: Body, g: Gait, far: boolean, o: { fore: number; hin const fu = u + reach * o.reach * g.go const fv = -lift * o.lift * g.go - r2 const Q = (a: number, b: number) => P(a, b, w) - const parts = limb(Q, u, -o.top, fu, fv, o.a, o.b, u > 0 ? -1 : 1, r0 * S, r1 * S, r2 * S, o.color) + const parts = limb(Q, u, -o.top, fu, fv, o.a * k, o.b * k, u > 0 ? -1 : 1, r0 * S, r1 * S, r2 * S, o.color) return o.paw ? [...parts, cap(...Q(fu, fv), ...Q(fu + o.paw[0], fv + r2 * 0.1), r2 * 1.15 * S, r2 * S, o.paw[1])] : parts }) } diff --git a/wallpapers/prehistoric.ts b/wallpapers/prehistoric.ts index d37610a..eef6a05 100644 --- a/wallpapers/prehistoric.ts +++ b/wallpapers/prehistoric.ts @@ -1,4 +1,5 @@ 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 { bird, flyAway, startle, type Flier } from "../src/flock" import { TAU, clamp, fbm1, hash, hash2, lerp, rand, smoothstep, type RGB } from "../src/math" @@ -159,6 +160,7 @@ interface Walker { rest: number moving: boolean size: number + g: Gait } class Prehistoric extends Canvas { @@ -178,7 +180,7 @@ class Prehistoric extends Canvas { private rex = { x: -9, dir: 1, next: 2, phase: 0 } private dust: { x: number; y: number; vx: number; age: number }[] = [] // p runs from browsing the left tree (0) to browsing the right one (1). - private sauropod = { p: 0, face: -1, target: -1, mode: "browse" as "browse" | "turn" | "walk", timer: rand(4.5, 6), phase: 0, pose: 1 } + private sauropod = { p: 0, face: -1, target: -1, mode: "browse" as "browse" | "turn" | "walk", timer: rand(4.5, 6), g: gait(-1), pose: 1 } private herd: Walker[] = [] private dragonflies: { x: number; y: number; tx: number; ty: number; rest: number; face: number; phase: number }[] = [] private nessie = { t: -1, x: 0, y: 0, dir: 1, wait: 1 } @@ -196,7 +198,7 @@ class Prehistoric extends Canvas { 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 - const walker = (x: number, y: number, size: number): Walker => ({ x, y, wx: x, wy: y, wanderT: 0, face: 1, phase: Math.random() * TAU, graze: 0, rest: 0, moving: false, size }) + const walker = (x: number, y: number, size: number): Walker => ({ x, y, wx: x, wy: y, wanderT: 0, face: 1, phase: Math.random() * TAU, graze: 0, rest: 0, moving: false, size, g: gait(1, hash(x * 7 + y)) }) this.herd = [walker(0.3, 0.8, 1), walker(0.55, 0.84, 1.1), walker(0.42, 0.82, 0.65)] this.dragonflies = Array.from({ length: 4 }, () => ({ x: Math.random() * 1.5, y: rand(0.75, 0.95), tx: 0, ty: 0, rest: rand(0, 3), face: 1, phase: Math.random() * TAU })) } @@ -228,7 +230,11 @@ class Prehistoric extends Canvas { if (this.activity !== "calm") this.stepSauropod(cdt) if (this.activity !== "teeming") return const A = this.A - for (const w of this.herd) this.wander(w, cdt, [0.08 * A, 0.45 * A, 0.78, 0.87], 0.03, [6, 14]) + for (const w of this.herd) { + const [x0, y0] = [w.x, w.y] + this.wander(w, cdt, [0.08 * A, 0.45 * A, 0.78, 0.87], 0.03, [6, 14]) + stepGait(w.g, Math.hypot(w.x - x0, w.y - y0) / (0.13 * (0.6 + (w.y - 0.7) * 2.5) * w.size), cdt, w.face) + } for (const d of this.dragonflies) this.stepDragonfly(d, cdt) this.stepNessie(cdt) } @@ -577,6 +583,7 @@ class Prehistoric extends Canvas { // The sauropod browses one tree's crown, turns, plods over to the other tree and browses there. private stepSauropod(dt: number) { const s = this.sauropod + stepGait(s.g, s.mode === "walk" ? (0.05 * dt) / 0.06 : 0, dt, s.face) s.pose = clamp(s.pose + (s.mode === "browse" ? 0.5 : -0.5) * dt, 0, 1) if (s.mode === "browse") { s.timer -= dt @@ -595,7 +602,6 @@ class Prehistoric extends Canvas { const [from, to] = this.sauropodSpots() const speed = 0.05 / Math.max(0.05, Math.abs(to - from)) s.p = clamp(s.p + s.face * speed * dt, 0, 1) - s.phase += dt * 0.05 / 0.06 if ((s.face > 0 && s.p < 1) || (s.face < 0 && s.p > 0)) return s.mode = "browse" s.timer = rand(3, 6) @@ -609,40 +615,35 @@ class Prehistoric extends Canvas { private drawSauropod() { const H = this.H const s = this.sauropod + const g = s.g const [from, to] = this.sauropodSpots() - const bx = lerp(from, to, s.p) - const F = 0.668 - const f = s.face - const side = f < 0 ? -1 : 1 + const side = s.face < 0 ? -1 : 1 + const rise = bob(g.phase) * g.go * 0.004 + const P = body(lerp(from, to, s.p) * H, 0.668 * H, H, s.face) const skin = this.paint(SAUROPOD) - const dark = this.paint([SAUROPOD[0] * 0.75, SAUROPOD[1] * 0.75, SAUROPOD[2] * 0.75]) - const walking = s.mode === "walk" ? 1 : 0 - const P = (u: number, v: number): [number, number] => [(bx + u * f) * H, (F + v) * H] - const leg = (u: number, offset: number, color: RGB) => { - const p = (s.phase + offset) % 1 - const q = p < 0.5 ? p / 0.5 : (p - 0.5) / 0.5 - const du = walking * (p < 0.5 ? lerp(0.025, -0.025, q) : lerp(-0.025, 0.025, smoothstep(0, 1, q))) - const lift = walking * (p < 0.5 ? 0 : Math.sin(Math.PI * q) * 0.01) - return cap(...P(u, -0.075), ...P(u + du, -lift), 0.014 * H, 0.012 * H, color) - } - this.shape([leg(0.055, 0.5, dark), leg(-0.075, 0, dark)], this.lighting, 0.5) + const legs = { fore: 0.07, hind: 0.06, top: 0.085 - rise, w: 0.022, a: 0.038, b: 0.037, reach: 0.018, lift: 0.012, r: [0.02, 0.016, 0.015] as [number, number, number] } + this.shape(quadruped(P, g, true, { ...legs, color: this.paint([SAUROPOD[0] * 0.75, SAUROPOD[1] * 0.75, SAUROPOD[2] * 0.75]) }), this.lighting, 0.5) const browsing: [number, number] = [this.trees[side > 0 ? 1 : 0] - side * 0.035 + Math.sin(this.time * 0.4) * 0.006, (side > 0 ? 0.35 : 0.33) + Math.sin(this.time * 0.9) * 0.004] - const shoulder = P(0.07, -0.108) - const walkHead: [number, number] = [shoulder[0] / H + f * 0.11, shoulder[1] / H - 0.14] + const shoulder = P(0.07, -0.11 - rise) + // Walking, the head rides ahead of the shoulders, nodding a beat behind each step. + const walkHead = P(0.18, -0.25 - rise + Math.cos(TAU * 2 * g.phase - 1.5) * 0.004 * g.go) const pose = smoothstep(0, 1, s.pose) - const head: [number, number] = [lerp(walkHead[0], browsing[0], pose) * H, lerp(walkHead[1], browsing[1], pose) * H] + const head: [number, number] = [lerp(walkHead[0], browsing[0] * H, pose), lerp(walkHead[1], browsing[1] * H, pose)] const ctrl: [number, number] = [shoulder[0] + (head[0] - shoulder[0]) * 0.1, head[1] + (shoulder[1] - head[1]) * 0.6] - const parts: Part[] = [leg(0.075, 0, skin), leg(-0.055, 0.5, skin)] - parts.push(ell(...P(0, -0.09), Math.max(0.035, 0.115 * Math.abs(f)) * H, 0.05 * H, -0.12 * side * Math.abs(f), skin)) - const tail = (t: number): [number, number] => { - const sway = Math.sin(this.time * 0.5 + t * 2) * 0.01 * t - return P(lerp(-0.08, -0.29, t), lerp(-0.095, -0.03 + sway, t) + 0.03 * Math.sin(Math.PI * t)) - } - for (let i = 0; i < 6; i++) parts.push(cap(...tail(i / 6), ...tail((i + 1) / 6), lerp(0.03, 0.003, i / 6) * H, lerp(0.03, 0.003, (i + 1) / 6) * H, skin)) const neck = (t: number): [number, number] => [(1 - t) ** 2 * shoulder[0] + 2 * (1 - t) * t * ctrl[0] + t * t * head[0], (1 - t) ** 2 * shoulder[1] + 2 * (1 - t) * t * ctrl[1] + t * t * head[1]] - for (let i = 0; i < 8; i++) parts.push(cap(...neck(i / 8), ...neck((i + 1) / 8), lerp(0.028, 0.009, i / 8) * H, lerp(0.028, 0.009, (i + 1) / 8) * H, skin)) - parts.push(ell(head[0] + side * 0.008 * H, head[1], 0.017 * H, 0.0105 * H, side * 0.2, skin)) - this.shape(parts, this.lighting, 0.6) + // The tail trails a turn and sways a beat behind the steps. + const tail = (t: number) => P(lerp(-0.08, -0.29, t), lerp(-0.097 - rise, -0.03, t) + 0.03 * Math.sin(Math.PI * t) + (Math.sin(this.time * 0.5 + t * 2) * 0.01 + Math.sin(TAU * 2 * g.phase - t * 2) * 0.006 * g.go) * t, -g.lag * 0.06 * t * t) + this.shape( + [ + ...quadruped(P, g, false, { ...legs, color: skin }), + ell(...P(0, -0.092 - rise), P.len(0.115, 0.04), 0.05 * H, -0.12 * P.along, skin), + ...chain(tail, 6, 0.03 * H, 0.003 * H, skin), + ...chain(neck, 8, 0.028 * H, 0.009 * H, skin), + ell(head[0] + side * 0.008 * H, head[1], 0.017 * H, 0.0105 * H, side * 0.2, skin), + ], + this.lighting, + 0.6, + ) } // Now and then a T. rex stomps across the foreground, tail swaying, kicking up dust at each footfall. @@ -679,49 +680,51 @@ class Prehistoric extends Canvas { const r = this.rex if (r.x < -5) return const H = this.H - const S = 0.3 * H - const x = r.x * H - const y = 0.935 * H - const bob = Math.abs(Math.cos(r.phase * TAU)) * 0.015 - const P = (u: number, v: number): [number, number] => [x + u * r.dir * S, y + v * S] + const P = body(r.x * H, 0.935 * H, 0.3 * H, r.dir) + const S = P.S + const rise = bob(r.phase, 0.55) * 0.02 const skin = this.paint(REX) const dark = this.paint([REX[0] * 0.7, REX[1] * 0.7, REX[2] * 0.7]) + // A bird-like leg: a heavy thigh, the knee forward, a long shin back to the ankle, then the foot to splayed toes. const leg = (offset: number, color: RGB): Part[] => { - const p = (r.phase + offset) % 1 - const q = p < 0.5 ? p / 0.5 : (p - 0.5) / 0.5 - const fu = p < 0.5 ? lerp(0.17, -0.17, q) : lerp(-0.17, 0.17, smoothstep(0, 1, q)) - const lift = p < 0.5 ? 0 : Math.sin(Math.PI * q) * 0.07 - const hip: [number, number] = [0, -0.56 + bob] - const ankle: [number, number] = [fu - 0.06, -0.13 - lift] - const knee: [number, number] = [(hip[0] + ankle[0]) / 2 + 0.1, (hip[1] + ankle[1]) / 2] + const [reach, lift] = stride(r.phase, offset, 0.55) + const fu = reach * 0.19 + const fv = -lift * 0.08 + const ankle: [number, number] = [fu - 0.08, fv - 0.13] return [ - cap(...P(...hip), ...P(...knee), 0.11 * S, 0.06 * S, color), - cap(...P(...knee), ...P(...ankle), 0.055 * S, 0.035 * S, color), - cap(...P(...ankle), ...P(fu, -lift), 0.035 * S, 0.03 * S, color), - cap(...P(fu, -lift), ...P(fu + 0.1, -lift), 0.03 * S, 0.015 * S, color), + ...limb(P, 0, -0.57 - rise, ...ankle, 0.25, 0.27, -1, 0.14 * S, 0.065 * S, 0.04 * S, color), + cap(...P(...ankle), ...P(fu + 0.01, fv - 0.025), 0.04 * S, 0.03 * S, color), + cap(...P(fu - 0.01, fv - 0.022), ...P(fu + 0.12, fv - 0.012 - lift * 0.03), 0.032 * S, 0.012 * S, color), ] } this.shape(leg(0.5, dark), this.lighting, 0.5) - const parts = leg(0, skin) - const tail = (t: number): [number, number] => P(lerp(-0.18, -0.9, t), lerp(-0.62 + bob, -0.5, t) + Math.sin(r.phase * TAU + t * 2) * 0.03 * t) - for (let i = 0; i < 6; i++) parts.push(cap(...tail(i / 6), ...tail((i + 1) / 6), lerp(0.13, 0.012, i / 6) * S, lerp(0.13, 0.012, (i + 1) / 6) * S, skin)) - const nod = Math.sin(r.phase * TAU * 2) * 0.012 - parts.push( - ell(...P(0.04, -0.62 + bob), 0.3 * S, 0.15 * S, -0.12 * r.dir, skin), - cap(...P(0.24, -0.68 + bob), ...P(0.38, -0.78 + bob + nod), 0.1 * S, 0.075 * S, skin), - ell(...P(0.5, -0.8 + bob + nod), 0.15 * S, 0.075 * S, 0.06 * r.dir, skin), - cap(...P(0.42, -0.74 + bob + nod), ...P(0.62, -0.75 + bob + nod), 0.045 * S, 0.03 * S, skin), - cap(...P(0.27, -0.58 + bob), ...P(0.33, -0.5 + bob), 0.026 * S, 0.02 * S, skin), - cap(...P(0.33, -0.5 + bob), ...P(0.37, -0.53 + bob), 0.018 * S, 0.008 * S, skin), + const nod = Math.cos(TAU * 2 * r.phase - 1.2) * 0.015 + const jaw = Math.max(0, Math.sin(this.creatureTime * 0.9 + 1)) ** 8 * 0.05 + // The tail counterbalances the head and swings a beat behind each step. + const tail = (t: number) => P(lerp(-0.16, -0.95, t), lerp(-0.66 - rise, -0.57, t) - 0.04 * Math.sin(Math.PI * t) + Math.sin(TAU * 2 * r.phase - 1 - t * 2.5) * 0.03 * t) + const [hu, hv] = [0.47, -0.84 - rise + nod] + this.shape( + [ + ...leg(0, skin), + ...chain(tail, 7, 0.14 * S, 0.012 * S, skin), + ell(...P(0.04, -0.64 - rise), 0.29 * S, 0.15 * S, 0.12 * r.dir, skin), + ell(...P(0.2, -0.62 - rise), 0.13 * S, 0.14 * S, 0, skin), + cap(...P(0.24, -0.7 - rise), ...P(hu - 0.07, hv + 0.02), 0.11 * S, 0.075 * S, skin), + ell(...P(hu, hv), 0.12 * S, 0.075 * S, 0.1 * r.dir, skin), + cap(...P(hu + 0.04, hv - 0.01), ...P(hu + 0.19, hv + 0.025), 0.065 * S, 0.042 * S, skin), + cap(...P(hu - 0.02, hv + 0.05), ...P(hu + 0.16, hv + 0.06 + jaw), 0.05 * S, 0.026 * S, skin), + ...limb(P, 0.27, -0.6 - rise, 0.35, -0.53 - rise + nod * 0.5, 0.07, 0.07, 1, 0.026 * S, 0.02 * S, 0.014 * S, skin), + ], + this.lighting, + 0.7, ) - this.shape(parts, this.lighting, 0.7) this.shape( - [0, 1, 2, 3].map((k) => cap(...P(-0.16 + k * 0.1, -0.76 + bob), ...P(-0.13 + k * 0.1, -0.63 + bob), 0.02 * S, 0.01 * S, dark)), + [0, 1, 2, 3].map((k) => cap(...P(-0.16 + k * 0.1, -0.77 - rise), ...P(-0.13 + k * 0.1, -0.64 - rise), 0.02 * S, 0.01 * S, dark)), this.lighting, 0.2, ) const eye = this.paint([0.95, 0.7, 0.1]) - this.disc(...P(0.52, -0.84 + bob + nod), 0.016 * S, eye[0], eye[1], eye[2], 1) + this.disc(...P(hu + 0.05, hv - 0.035), 0.016 * S, eye[0], eye[1], eye[2], 1) } private stepDelorean(dt: number) { @@ -814,33 +817,38 @@ class Prehistoric extends Canvas { w.y += (dy / d) * speed * dt } - // A triceratops with its bony frill and three horns, lowering its head to crop the ferns. + // A triceratops with its bony frill and three horns, plodding on stout legs and lowering its head to crop the ferns. private drawTriceratops(w: Walker) { const H = this.H - const S = 0.2 * H * (0.6 + (w.y - 0.7) * 2.5) * w.size - const x = w.x * H - const y = w.y * H - const P = (u: number, v: number): [number, number] => [x + u * w.face * S, y + v * S] - const swing = w.moving ? Math.sin(w.phase) * 0.06 : 0 + const g = w.g + const P = body(w.x * H, w.y * H, 0.27 * H * (0.6 + (w.y - 0.7) * 2.5) * w.size, g.turn) + const S = P.S const skin = this.paint(TRIKE) - const g = w.graze - const [hu, hv] = [lerp(0.5, 0.58, g), lerp(-0.5, -0.24, g)] - const parts: Part[] = [] - for (const [u, s] of [[0.24, 1], [0.3, -1], [-0.22, -1], [-0.28, 1]] as const) parts.push(cap(...P(u, -0.3), ...P(u + swing * s, 0), 0.07 * S, 0.06 * S, skin)) - parts.push( - cap(...P(-0.36, -0.44), ...P(-0.68, -0.3), 0.09 * S, 0.015 * S, skin), - ell(...P(0, -0.44), 0.42 * S, 0.24 * S, 0, skin), - ell(...P(hu - 0.08, hv - 0.1), 0.12 * S, 0.19 * S, lerp(-0.45, -0.1, g) * w.face, this.paint([0.62, 0.34, 0.18])), - ell(...P(hu + 0.06, hv + 0.02), 0.14 * S, 0.085 * S, lerp(0.25, 0.8, g) * w.face, skin), - cap(...P(hu + 0.17, hv + 0.06), ...P(hu + 0.23, hv + 0.11), 0.04 * S, 0.012 * S, skin), + const rise = bob(g.phase) * g.go * 0.012 + const legs = { fore: 0.24, hind: 0.24, top: 0.32 - rise, w: 0.11, a: 0.17, b: 0.16, reach: 0.15, lift: 0.07, r: [0.1, 0.065, 0.055] as [number, number, number], paw: [0.03, this.paint([0.4, 0.33, 0.21])] as [number, RGB] } + this.shape(quadruped(P, g, true, { ...legs, color: this.paint([TRIKE[0] * 0.75, TRIKE[1] * 0.75, TRIKE[2] * 0.75]) }), this.lighting, 0.7) + const gz = smoothstep(0, 1, w.graze) + const nod = Math.cos(TAU * 2 * g.phase - 1) * 0.02 * g.go + const [hu, hv] = [lerp(0.48, 0.56, gz), lerp(-0.44, -0.22, gz) + nod - rise] + const tail = (t: number) => P(lerp(-0.26, -0.74, t), lerp(-0.48 - rise, -0.3, t) + Math.sin(TAU * 2 * g.phase - t * 2) * 0.025 * t * g.go, -g.lag * 0.08 * t * t) + this.shape( + [ + ...quadruped(P, g, false, { ...legs, color: skin }), + ...chain(tail, 5, 0.15 * S, 0.015 * S, skin), + ell(...P(-0.02, -0.46 - rise), P.len(0.4, 0.2), 0.23 * S, 0.08 * P.along, skin), + ell(...P(hu - 0.08, hv - 0.1), P.len(0.11, 0.18), 0.19 * S, lerp(-0.45, -0.1, gz) * P.along, this.paint([0.62, 0.34, 0.18])), + cap(...P(hu - 0.02, hv - 0.02), ...P(hu + 0.19, hv + 0.07), 0.1 * S, 0.045 * S, skin), + cap(...P(hu + 0.17, hv + 0.06), ...P(hu + 0.23, hv + 0.12), 0.04 * S, 0.012 * S, this.paint([0.3, 0.25, 0.17])), + ], + this.lighting, + 0.7, ) - this.shape(parts, this.lighting, 0.7) const bone = this.paint(BONE) this.shape( [ - cap(...P(hu + 0.02, hv - 0.04), ...P(hu + 0.28, hv - 0.2 + g * 0.14), 0.025 * S, 0.004 * S, bone), - cap(...P(hu + 0.06, hv - 0.03), ...P(hu + 0.31, hv - 0.15 + g * 0.14), 0.022 * S, 0.004 * S, bone), - cap(...P(hu + 0.16, hv + 0.01), ...P(hu + 0.2, hv - 0.06), 0.02 * S, 0.004 * S, bone), + cap(...P(hu + 0.02, hv - 0.06), ...P(hu + 0.28, hv - 0.22 + gz * 0.14), 0.026 * S, 0.004 * S, bone), + cap(...P(hu + 0.05, hv - 0.05, 0.05), ...P(hu + 0.31, hv - 0.17 + gz * 0.14, 0.08), 0.022 * S, 0.004 * S, bone), + cap(...P(hu + 0.15, hv + 0.01), ...P(hu + 0.19, hv - 0.07), 0.022 * S, 0.004 * S, bone), ], this.lighting, 0.5, From daff6a7280441ecc322098026abfbb19cd07fa38 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:42:33 -0700 Subject: [PATCH 03/10] Walk the tundra animals on jointed legs and bring the fox down to size --- wallpapers/tundra.ts | 121 +++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/wallpapers/tundra.ts b/wallpapers/tundra.ts index f7d1dd3..48a415d 100644 --- a/wallpapers/tundra.ts +++ b/wallpapers/tundra.ts @@ -1,4 +1,5 @@ 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 { TAU, clamp, fbm1, hash, 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" @@ -125,6 +126,7 @@ interface Walker { moving: boolean // Creature time into a hop after a click; negative while waiting its turn, Infinity when not hopping. hop: number + g: Gait } class Tundra extends Canvas { @@ -157,7 +159,7 @@ class Tundra extends Canvas { // Its own gentle snowfall under a clear sky, unless weather is chosen. this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, HORIZON) 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 }) + 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, g: gait(1, hash(x * 7 + y)) }) if (settings.activity !== "calm") { this.penguins = Array.from({ length: settings.activity === "teeming" ? 5 : 3 }, (_, i) => walker(0.6 + i * 0.12, 0.78 + (i % 2) * 0.015)) this.fox = walker(0.9, 0.88) @@ -185,9 +187,9 @@ class Tundra extends Canvas { } this.stepOwl(dt) const A = this.A - for (const p of this.penguins) this.wander(p, cdt, [0.34 * A, 0.58 * A, 0.77, 0.81], 0.025) - if (this.fox) this.wander(this.fox, cdt, [0.3 * A, 0.95 * A, 0.84, 0.92], 0.04) - if (this.bear) this.wander(this.bear, cdt, [0.25 * A, 0.85 * A, 0.648, 0.662], 0.025) + for (const p of this.penguins) this.wander(p, cdt, [0.34 * A, 0.58 * A, 0.77, 0.81], 0.025, 0.012) + if (this.fox) this.wander(this.fox, cdt, [0.3 * A, 0.95 * A, 0.84, 0.92], 0.04, 0.03) + if (this.bear) this.wander(this.bear, cdt, [0.25 * A, 0.85 * A, 0.648, 0.662], 0.025, 0.08) const f = this.fisher f.next -= cdt if (f.next <= 0) { @@ -440,7 +442,14 @@ class Tundra extends Canvas { } } - private wander(w: Walker, dt: number, zone: [number, number, number, number], speed: number) { + // stride is the distance a full step cycle covers at the front of the snowfield, so the legs keep pace with the ground. + private wander(w: Walker, dt: number, zone: [number, number, number, number], speed: number, stride: number) { + const [x0, y0] = [w.x, w.y] + this.roam(w, dt, zone, speed) + stepGait(w.g, Math.hypot(w.x - x0, w.y - y0) / (stride * this.depthScale(w.y)), dt, w.face) + } + + private roam(w: Walker, dt: number, zone: [number, number, number, number], speed: number) { w.phase += dt * (w.moving ? 6 : 1) if (w.rest > 0) { w.rest -= dt @@ -471,33 +480,46 @@ class Tundra extends Canvas { return 0.55 + (y - 0.65) * 2.5 } - // A penguin waddling side to side, flippers out. + // A penguin waddling side to side on short steps, flippers out and trailing as it sets off and stops; its white + // front comes round as it turns. private drawPenguin(p: Walker) { const H = this.H - const S = 0.07 * H * this.depthScale(p.y) - const rock = p.moving ? Math.sin(p.phase) * 0.14 : Math.sin(p.phase) * 0.03 + const g = p.g + const S = 0.075 * H * this.depthScale(p.y) + const rock = Math.sin(TAU * g.phase) * 0.14 * g.go + Math.sin(p.phase) * 0.03 * (1 - g.go) const x = p.x * H const y = (p.y - (p.hop >= 0 && p.hop < HOP ? Math.abs(Math.sin((p.hop / HOP) * TAU)) * 0.025 * this.depthScale(p.y) : 0)) * H + const [al, ac] = view(g.turn) const cr = Math.cos(rock) const sr = Math.sin(rock) - const P = (u: number, v: number): [number, number] => [x + (u * p.face * cr - v * sr) * S, y + (u * p.face * sr + v * cr) * S] + // Rocks about the feet, which stay put. + const P = (u: number, v: number, w = 0): [number, number] => { + const sx = u * al - w * ac + return [x + (sx * cr - v * sr) * S, y + (sx * sr + v * cr) * S] + } + const len = (u: number, w: number) => (u * Math.abs(al) + w * ac) * S const black = this.paint(DARK) const belly = this.paint([0.92, 0.92, 0.95]) const orange = this.paint(ORANGE) + const feet = [-1, 1].map((w) => { + const [reach, lift] = stride(g.phase, w > 0 ? 0 : 0.5) + const fu = reach * 0.06 * g.go + return ell(...P(fu + 0.05, -0.02 - lift * 0.05 * g.go, w * 0.09), len(0.1, 0.05), 0.035 * S, 0, orange) + }) + const flip = 0.1 + g.lag * 0.12 + Math.abs(rock) * 0.3 this.shape( [ - ell(...P(0.06, -0.02), 0.1 * S, 0.035 * S, 0, orange), - ell(...P(-0.08, -0.02), 0.1 * S, 0.035 * S, 0, orange), - ell(...P(0, -0.42), 0.22 * S, 0.4 * S, rock, black), - cap(...P(-0.05, -0.62), ...P(-0.2, -0.32), 0.06 * S, 0.03 * S, black), - ell(...P(0.03, -0.82), 0.15 * S, 0.14 * S, 0, black), + ...feet, + ell(...P(0, -0.42), len(0.22, 0.2), 0.4 * S, rock, black), + ...[-1, 1].map((w) => cap(...P(-0.04, -0.62, w * 0.17), ...P(-0.14 - flip * 0.3, -0.32, w * (0.2 + flip)), 0.06 * S, 0.025 * S, black)), + ell(...P(0.04, -0.82), len(0.15, 0.13), 0.14 * S, 0, black), ], this.lighting, 0.6, ) - this.shape([ell(...P(0.07, -0.38), 0.13 * S, 0.3 * S, rock, belly)], this.lighting, 0.6) - this.shape([cap(...P(0.13, -0.84), ...P(0.26, -0.81), 0.035 * S, 0.012 * S, orange)], this.lighting, 0.5) - this.add(...P(0.09, -0.86), 0.5, 0.5, 0.5) + this.shape([ell(...P(0.08, -0.38), len(0.13, 0.15), 0.3 * S, rock, belly)], this.lighting, 0.6) + this.shape([cap(...P(0.14, -0.84), ...P(0.27, -0.81), 0.035 * S, 0.012 * S, orange)], this.lighting, 0.5) + for (const w of ac > 0.3 ? [-1, 1] : [0]) this.add(...P(0.1, -0.86, w * 0.06), 0.5, 0.5, 0.5) } // A shaggy silhouette on the far snowfield, fading in and out of the falling snow. It stands, looks around, shuffles @@ -533,60 +555,67 @@ class Tundra extends Canvas { if (look.style === "rim") for (const s of [-1, 1]) this.add(...P(turn + s * 0.035, -0.99), 0.5 * fade, 0.3 * fade, 0.04 * fade) } - // An arctic fox trotting across the snow, bushy tail out behind. + // An arctic fox trotting across the snow on slim legs, ears up and bushy tail streaming out behind. private drawFox(f: Walker) { const H = this.H - const S = 0.08 * H * this.depthScale(f.y) - const x = f.x * H - const y = f.y * H - const P = (u: number, v: number): [number, number] => [x + u * f.face * S, y + v * S] + const g = f.g + const P = body(f.x * H, f.y * H, 0.05 * H * this.depthScale(f.y), g.turn) + const S = P.S const fur = this.paint(FUR) - const swing = f.moving ? Math.sin(f.phase) * 0.08 : 0 + const rise = bob(g.phase) * g.go * 0.02 + const legs = { fore: 0.21, hind: 0.21, top: 0.3 - rise, w: 0.06, a: 0.5, b: 0.5, reach: 0.18, lift: 0.09, r: [0.065, 0.032, 0.028] as [number, number, number], paw: [0.035, fur] as [number, RGB] } + this.shape(quadruped(P, g, true, { ...legs, color: this.paint([0.72, 0.72, 0.7]) }), this.lighting, 0.7) + const nod = Math.cos(TAU * 2 * g.phase - 1) * 0.015 * g.go - rise + const flick = Math.sin(TAU * 2 * g.phase - 2) * 0.03 * g.go - g.lag * 0.05 + const tail = (t: number) => P(-0.26 - t * 0.4, -0.4 - rise + t * 0.12 - Math.sin(Math.PI * t) * 0.06 + flick * t * t) this.shape( [ - ...[[0.22, 1], [0.27, -1], [-0.2, -1], [-0.25, 1]].map(([u, s]) => cap(...P(u, -0.28), ...P(u + swing * s, 0), 0.035 * S, 0.028 * S, fur)), - cap(...P(-0.32, -0.4), ...P(-0.7, -0.3), 0.1 * S, 0.06 * S, fur), - ell(...P(0, -0.38), 0.34 * S, 0.14 * S, 0, fur), - cap(...P(0.28, -0.42), ...P(0.4, -0.52), 0.09 * S, 0.08 * S, fur), - ell(...P(0.44, -0.55), 0.11 * S, 0.09 * S, 0, fur), - cap(...P(0.5, -0.53), ...P(0.62, -0.5), 0.05 * S, 0.02 * S, fur), - cap(...P(0.4, -0.62), ...P(0.36, -0.74), 0.035 * S, 0.006 * S, fur), - cap(...P(0.47, -0.62), ...P(0.46, -0.75), 0.035 * S, 0.006 * S, fur), + ...quadruped(P, g, false, { ...legs, color: fur }), + ell(...P(0, -0.4 - rise), P.len(0.3, 0.11), 0.12 * S, 0, fur), + ...chain(tail, 3, 0.07 * S, 0.1 * S, fur), + ell(...tail(0.95), P.len(0.09, 0.08), 0.08 * S, 0, fur), + cap(...P(0.22, -0.43 - rise), ...P(0.36, -0.54 + nod), 0.1 * S, 0.075 * S, fur), + ell(...P(0.4, -0.56 + nod), 0.09 * S, 0.08 * S, 0, fur), + cap(...P(0.44, -0.54 + nod), ...P(0.58, -0.5 + nod), 0.05 * S, 0.016 * S, fur), + ...[-1, 1].map((w) => cap(...P(0.38, -0.62 + nod, w * 0.05), ...P(0.36 - g.lag * 0.02, -0.76 + nod, w * 0.07), 0.035 * S, 0.006 * S, fur)), ], this.lighting, 0.7, ) - this.add(...P(0.62, -0.5), 0.02, 0.02, 0.02) + this.add(...P(0.58, -0.5 + nod), 0.02, 0.02, 0.02) } - // A polar bear plodding along the far shore of the lake, head low. + // A polar bear plodding along the far shore of the lake, head low and swinging, rump high, on big furry paws. private drawBear(b: Walker) { const H = this.H - const S = 0.13 * H * this.depthScale(b.y) - const x = b.x * H - const y = b.y * H - const P = (u: number, v: number): [number, number] => [x + u * b.face * S, y + v * S] + const g = b.g + const P = body(b.x * H, b.y * H, 0.16 * H * this.depthScale(b.y), g.turn) + const S = P.S const fur = this.paint([0.95, 0.9, 0.74]) - const swing = b.moving ? Math.sin(b.phase * 0.8) * 0.06 : 0 + const rise = bob(g.phase) * g.go * 0.015 + const legs = { fore: 0.24, hind: 0.24, top: 0.33 - rise, w: 0.09, a: 0.5, b: 0.5, reach: 0.15, lift: 0.07, r: [0.11, 0.075, 0.07] as [number, number, number], paw: [0.05, fur] as [number, RGB] } + this.shape(quadruped(P, g, true, { ...legs, color: this.paint([0.76, 0.72, 0.6]) }), this.lighting, 0.7) + const sway = Math.sin(TAU * g.phase - 1) * 0.04 * g.go + const nod = Math.cos(TAU * 2 * g.phase - 1) * 0.015 * g.go - rise this.shape( [ - ...[[0.25, 1], [0.3, -1], [-0.22, -1], [-0.27, 1]].map(([u, s]) => cap(...P(u, -0.3), ...P(u + swing * s, 0), 0.07 * S, 0.06 * S, fur)), - ell(...P(0, -0.38), 0.38 * S, 0.2 * S, 0, fur), - ell(...P(-0.22, -0.42), 0.18 * S, 0.17 * S, 0, fur), - cap(...P(0.3, -0.42), ...P(0.46, -0.34), 0.12 * S, 0.09 * S, fur), - ell(...P(0.5, -0.32), 0.1 * S, 0.08 * S, 0.2 * b.face, fur), - ell(...P(0.43, -0.42), 0.035 * S, 0.035 * S, 0, fur), + ...quadruped(P, g, false, { ...legs, color: fur }), + ell(...P(-0.02, -0.4 - rise), P.len(0.36, 0.17), 0.19 * S, 0.08 * P.along, fur), + cap(...P(0.24, -0.44 - rise), ...P(0.46, -0.36 + nod, sway), 0.14 * S, 0.09 * S, fur), + cap(...P(0.46, -0.37 + nod, sway), ...P(0.62, -0.32 + nod, sway), 0.085 * S, 0.045 * S, fur), + ...[-1, 1].map((w) => ell(...P(0.45, -0.45 + nod, sway + w * 0.05), 0.035 * S, 0.035 * S, 0, fur)), + ell(...P(-0.38, -0.46 - rise), 0.04 * S, 0.035 * S, 0, fur), ], this.lighting, 0.7, ) - this.add(...P(0.6, -0.31), 0.02, 0.02, 0.02) + this.add(...P(0.63, -0.33 + nod, sway), 0.02, 0.02, 0.02) } // Someone in a red parka ice fishing on the lake, rod tugging now and then. private drawFisher() { const H = this.H - const S = 0.12 * H + const S = 0.1 * H const hx = 0.68 * this.A * H const hy = 0.718 * H const x = hx + 0.06 * H From a4b0fd4b610b37ea1edc28d66bd546655d429be0 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:45:21 -0700 Subject: [PATCH 04/10] Shape the desert coyote and let its head and ears settle after a howl --- wallpapers/desert.ts | 47 +++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/wallpapers/desert.ts b/wallpapers/desert.ts index 570ddf3..ac696f0 100644 --- a/wallpapers/desert.ts +++ b/wallpapers/desert.ts @@ -1,4 +1,5 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" +import { chain } from "../src/creature" 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" @@ -245,6 +246,9 @@ interface Snake { interface Coyote { howl: number + // Where the head is between resting (0) and howling (1): it chases howl on a spring, so it lags and overshoots. + head: number + headV: number t: number next: number } @@ -306,7 +310,7 @@ class Desert extends Canvas { this.flock = look.night ? [0, 1.6, 3.1, 4.7].map((angle, i) => ({ angle, speed: 0.9 + i * 0.12, cx: 0.72, cy: 0.3, rx: 0.06 + i * 0.03, ry: 0.04, phase: i * 1.7 })) : [0, 2.1, 4.2].map((angle, i) => ({ angle, speed: 0.4 + i * 0.05, cx: 0.25, cy: 0.16, rx: 0.08 + i * 0.035, ry: 0.025, phase: i })) - this.coyote = { howl: 0, t: -1, next: 8 } + this.coyote = { howl: 0, head: 0, headV: 0, t: -1, next: 8 } } } @@ -624,6 +628,8 @@ class Desert extends Canvas { } } c.howl = c.t < 0 ? 0 : smoothstep(0, 1.5, c.t) * (1 - smoothstep(4.5, 6, c.t)) + c.headV += ((c.howl - c.head) * 30 - c.headV * 3.5) * dt + c.head += c.headV * dt } } @@ -779,7 +785,8 @@ class Desert extends Canvas { const flex = 0.006 * Math.sin(b.phase * 1.3) const parts: Part[] = [] for (const side of [-1, 1]) { - const spine = (t: number): [number, number] => [side * (0.028 + 0.125 * t), -dihedral * t * t + flex * t * t] + // The wingtips lag the roll, bending against the way it is banking. + const spine = (t: number): [number, number] => [side * (0.028 + 0.125 * t), -dihedral * t * t + flex * t * t + side * 0.025 * Math.cos(b.angle) * t * t * t] for (let i = 0; i < 6; i++) parts.push(cap(...T(...spine(i / 6)), ...T(...spine((i + 1) / 6)), chord * (1 - 0.3 * (i / 6)) * S, chord * (1 - 0.3 * ((i + 1) / 6)) * S, main)) const [tu, tv] = spine(0.92) if (kind === "owl") { @@ -836,7 +843,8 @@ class Desert extends Canvas { this.silhouette(parts, 0.35) } - // A coyote sitting on top of a mesa, facing the sun or moon, that now and then lifts its head to howl. + // A coyote sitting on top of a mesa, facing the sun or moon, that now and then lifts its head to howl. The head + // swings up and settles back a little past rest, ears trailing it, and the brush of a tail sweeps now and then. private drawCoyote(c: Coyote) { const x = this.coyoteX + 0.5 const gy = this.nearTop[this.coyoteX] + 0.5 @@ -845,24 +853,27 @@ class Desert extends Canvas { const P = (u: number, v: number): [number, number] => [x + u * face * S, gy + v * S] const k = this.look.coyote // The head turns from looking ahead to pointing at the sky; R places points in the head's frame. - const a = lerp(-0.2, -1.2, c.howl) - const head = [0.21, -0.66] + const a = lerp(-0.15, -1.2, c.head) + const head = [0.2, -0.66] const R = (u: number, v: number): [number, number] => P(head[0] + u * Math.cos(a) - v * Math.sin(a), head[1] + u * Math.sin(a) + v * Math.cos(a)) + const ears = 0.4 * c.head + (c.howl - c.head) * 2 + const sweep = Math.sin(this.time * CREATURE_SPEED * 1.7) ** 3 * 0.04 + const tail = (t: number) => P(-0.18 - t * 0.3, -0.07 + t * 0.04 - Math.sin(Math.PI * t) * 0.04 + sweep * t) this.silhouette( [ - cap(...P(-0.17, -0.06), ...P(-0.3, -0.02), 0.06 * S, 0.055 * S, k), - cap(...P(-0.3, -0.02), ...P(-0.42, -0.01), 0.055 * S, 0.03 * S, k), - ell(...P(-0.05, -0.17), 0.17 * S, 0.17 * S, 0, k), - cap(...P(-0.02, -0.22), ...P(0.12, -0.48), 0.13 * S, 0.11 * S, k), - cap(...P(0.12, -0.45), ...P(0.16, -0.12), 0.08 * S, 0.055 * S, k), - cap(...P(0.15, -0.15), ...P(0.17, 0), 0.035 * S, 0.03 * S, k), - cap(...P(0.11, -0.15), ...P(0.12, 0), 0.03 * S, 0.028 * S, k), - ell(...P(0.04, -0.025), 0.1 * S, 0.03 * S, 0, k), - cap(...P(0.12, -0.5), ...P(head[0], head[1]), 0.07 * S, 0.06 * S, k), - ell(...R(0, 0), 0.08 * S, 0.062 * S, a * face, k), - cap(...R(0, 0), ...R(0.15, 0.01), 0.045 * S, 0.02 * S, k), - cap(...R(-0.025, -0.045), ...R(-0.055, -0.135), 0.026 * S, 0.004 * S, k), - cap(...R(0.005, -0.05), ...R(-0.015, -0.14), 0.024 * S, 0.004 * S, k), + ...chain(tail, 3, 0.05 * S, 0.065 * S, k), + ell(...tail(1), 0.06 * S, 0.04 * S, 0, k), + ell(...P(-0.08, -0.15), 0.16 * S, 0.15 * S, 0, k), + ell(...P(0.02, -0.03), 0.12 * S, 0.03 * S, 0, k), + cap(...P(-0.04, -0.2), ...P(0.1, -0.48), 0.14 * S, 0.105 * S, k), + cap(...P(0.12, -0.42), ...P(0.16, -0.03), 0.055 * S, 0.03 * S, k), + cap(...P(0.08, -0.42), ...P(0.11, -0.03), 0.05 * S, 0.028 * S, k), + ell(...P(0.18, -0.02), 0.045 * S, 0.022 * S, 0, k), + cap(...P(0.1, -0.48), ...P(head[0], head[1]), 0.085 * S, 0.06 * S, k), + ell(...R(0, 0), 0.075 * S, 0.058 * S, a * face, k), + cap(...R(0.02, 0.005), ...R(0.17, 0.02), 0.042 * S, 0.014 * S, k), + cap(...R(-0.025, -0.04), ...R(-0.05 - ears * 0.05, -0.15 + ears * 0.03), 0.03 * S, 0.004 * S, k), + cap(...R(0.01, -0.045), ...R(-0.005 - ears * 0.05, -0.15 + ears * 0.03), 0.027 * S, 0.004 * S, k), ], 1.3, ) From 2572179e40e780fc678cb91d727360b39a0ed9e6 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:47:31 -0700 Subject: [PATCH 05/10] Shape the jungle jaguar and let the monkey's limbs trail its swing --- wallpapers/jungle.ts | 73 +++++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/wallpapers/jungle.ts b/wallpapers/jungle.ts index 850f37c..7e638b9 100644 --- a/wallpapers/jungle.ts +++ b/wallpapers/jungle.ts @@ -1,4 +1,5 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" +import { limb } from "../src/creature" import { eggWait } from "../src/egg" import { TAU, clamp, fbm1, hash, lerp, rand, smoothstep, type RGB } from "../src/math" import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper" @@ -140,6 +141,8 @@ class Jungle extends Canvas { private pollen = Array.from({ length: 40 }, () => ({ x: Math.random() * 4, y: 0.1 + Math.random() * 0.7, s: Math.random() })) // A click sets the monkey swinging harder: kick is how hard, fading away, and swing is the phase of that swing. private monkey = { kick: 0, swing: 0 } + // How far the jaguar has lifted its head to look about: it springs toward that now and then and sinks back. + private jaguarHead = { p: 0, v: 0 } // The vine in the left corner, in screen heights, which the sloth climbs down. private liana = { x: 0, length: 0, phase: 0 } // The easter egg: a sloth lowers itself down the vine, smiles at you, and climbs back up. t is creature time. @@ -187,6 +190,9 @@ class Jungle extends Canvas { const m = this.monkey m.kick *= Math.exp(-cdt * 0.5) m.swing += cdt * 6 + const j = this.jaguarHead + j.v += ((Math.max(0, Math.sin(this.creatureTime * 0.35)) ** 12 - j.p) * 25 - j.v * 3) * cdt + j.p += j.v * cdt const sl = this.sloth if (sl.t < 0) { sl.wait -= dt @@ -480,29 +486,34 @@ class Jungle extends Canvas { ) } - // A spider monkey hanging by its tail from the right-hand branch, swinging slowly; at night it barely stirs. + // A spider monkey hanging by its tail from the right-hand branch, swinging slowly, its long arms and legs trailing + // each swing; at night it barely stirs. private drawMonkey() { const H = this.H const [ax, ay] = this.rightBranch(0.71 * this.A) const m = this.monkey - const swing = (this.look.night ? 0.05 : 0.35) * Math.sin(this.creatureTime * 1.2) + (this.look.night ? 0.2 : 0.4) * m.kick * Math.sin(m.swing) + const night = this.look.night + const swingAt = (t: number) => (night ? 0.05 : 0.35) * Math.sin(t * 1.2) + (night ? 0.2 : 0.4) * m.kick * Math.sin(m.swing - (this.creatureTime - t) * 6) + const swing = swingAt(this.creatureTime) + // Limbs hang a beat behind the body, so they trail each swing and catch up at its end. + const trail = (swingAt(this.creatureTime - 0.3) - swing) * 1.4 const cs = Math.cos(swing) const sn = Math.sin(swing) const P = (u: number, v: number): [number, number] => [ax + (u * cs - v * sn) * H, ay + (u * sn + v * cs) * H] + const hang = (u0: number, v0: number, u: number, v: number): [number, number] => [u0 + (u - u0) * Math.cos(trail) - (v - v0) * Math.sin(trail), v0 + (u - u0) * Math.sin(trail) + (v - v0) * Math.cos(trail)] const fur = this.paint([0.3, 0.2, 0.12]) const parts: Part[] = [cap(ax - 0.008 * H, ay - 0.01 * H, ax + 0.008 * H, ay - 0.01 * H, 0.004 * H, 0.004 * H, fur)] for (let i = 0; i < 4; i++) parts.push(cap(...P(Math.sin(i * 0.8) * 0.006, (i / 4) * 0.07), ...P(Math.sin((i + 1) * 0.8) * 0.006, ((i + 1) / 4) * 0.07), 0.004 * H, 0.004 * H, fur)) parts.push( - cap(...P(0, 0.07), ...P(0, 0.14), 0.018 * H, 0.016 * H, fur), - cap(...P(0, 0.075), ...P(-0.025, 0.045), 0.006 * H, 0.005 * H, fur), - cap(...P(0, 0.075), ...P(0.025, 0.05), 0.006 * H, 0.005 * H, fur), - cap(...P(-0.006, 0.135), ...P(-0.03, 0.2), 0.006 * H, 0.005 * H, fur), - cap(...P(0.006, 0.135), ...P(0.04, 0.19), 0.006 * H, 0.005 * H, fur), - ell(...P(0, 0.165), 0.02 * H, 0.019 * H, swing, fur), - ell(...P(0, 0.17), 0.012 * H, 0.011 * H, swing, this.paint([0.65, 0.5, 0.38])), + cap(...P(0, 0.07), ...P(0, 0.14), 0.018 * H, 0.02 * H, fur), + ...[-1, 1].flatMap((s) => limb(P, s * 0.006, 0.078, ...hang(s * 0.006, 0.078, s * 0.035, 0.05), 0.03, 0.03, s, 0.007 * H, 0.005 * H, 0.004 * H, fur)), + ...[-1, 1].flatMap((s) => limb(P, s * 0.008, 0.135, ...hang(s * 0.008, 0.135, s * 0.03, 0.225), 0.05, 0.05, -s, 0.006 * H, 0.005 * H, 0.004 * H, fur)), + ell(...P(0, 0.165), 0.019 * H, 0.019 * H, swing, fur), + ...[-1, 1].map((s) => ell(...P(s * 0.018, 0.16), 0.006 * H, 0.006 * H, 0, fur)), + ell(...P(0, 0.172), 0.012 * H, 0.011 * H, swing, this.paint([0.65, 0.5, 0.38])), ) this.shape(parts, this.lighting, 0.7) - for (const s of [-1, 1]) this.add(...P(s * 0.005, 0.167), 0.01, 0.01, 0.01) + for (const s of [-1, 1]) this.add(...P(s * 0.005, 0.169), 0.01, 0.01, 0.01) } // A three-toed sloth hanging from the corner vine by its long arms, lowering itself hand over hand out of the @@ -568,7 +579,8 @@ class Jungle extends Canvas { this.add(...P(0.07, -0.7 + bob), 0.01, 0.01, 0.01) } - // A jaguar draped along the left-hand branch, legs dangling, breathing slowly as its tail sways. + // A jaguar draped along the left-hand branch, chin on one paw and the other legs dangling, breathing slowly as its + // tail sways. Now and then it lifts its head to look about, and lets it sink back to rest. private drawJaguar() { const { A, H } = this const top = (x: number): [number, number] => { @@ -577,27 +589,38 @@ class Jungle extends Canvas { } const breathe = 1 + 0.03 * Math.sin(this.creatureTime * 1.5) const coat = this.paint([0.85, 0.58, 0.22]) - const [hx, hy] = top(0.445) - const [rx, ry] = top(0.29) - const [fx, fy] = top(0.415) + const pale = this.paint([0.92, 0.82, 0.62]) + const lift = this.jaguarHead.p + const [rx, ry] = top(0.3) + const [fx, fy] = top(0.4) + const [hx, hy] = [top(0.445)[0] - lift * 0.004 * H, top(0.445)[1] - lift * 0.022 * H] + // Dangling legs swing gently, the paws a beat behind. + const dangle = (t: number) => Math.sin(this.creatureTime * 0.6 - t) * 0.006 * H const parts: Part[] = [ - cap(...top(0.29), ...top(0.41), 0.03 * H * breathe, 0.028 * H * breathe, coat), - cap(fx, fy + 0.015 * H, fx + 0.004 * H, fy + 0.085 * H, 0.011 * H, 0.009 * H, coat), - cap(fx - 0.02 * H, fy + 0.015 * H, fx - 0.018 * H, fy + 0.07 * H, 0.01 * H, 0.008 * H, coat), - cap(rx + 0.01 * H, ry + 0.015 * H, rx + 0.012 * H, ry + 0.08 * H, 0.012 * H, 0.009 * H, coat), - ell(hx, hy - 0.005 * H, 0.026 * H, 0.023 * H, 0, coat), - ell(hx + 0.016 * H, hy + 0.004 * H, 0.012 * H, 0.009 * H, 0, this.paint([0.92, 0.82, 0.62])), - cap(hx - 0.014 * H, hy - 0.02 * H, hx - 0.016 * H, hy - 0.034 * H, 0.007 * H, 0.004 * H, coat), - cap(hx + 0.006 * H, hy - 0.024 * H, hx + 0.008 * H, hy - 0.038 * H, 0.007 * H, 0.004 * H, coat), + ell(rx, ry - 0.002 * H, 0.033 * H, 0.03 * H * breathe, -0.08, coat), + cap(...top(0.31), ...top(0.39), 0.025 * H * breathe, 0.027 * H * breathe, coat), + ell(fx, fy, 0.03 * H, 0.03 * H * breathe, 0, coat), + cap(fx + 0.012 * H, fy - 0.006 * H, hx - 0.012 * H, hy + 0.004 * H, 0.02 * H, 0.016 * H, coat), + cap(fx + 0.008 * H, fy + 0.012 * H, fx + 0.036 * H, fy + 0.03 * H, 0.011 * H, 0.009 * H, coat), + ell(fx + 0.042 * H, fy + 0.031 * H, 0.012 * H, 0.007 * H, 0, coat), + cap(fx - 0.012 * H, fy + 0.016 * H, fx - 0.008 * H + dangle(0), fy + 0.05 * H, 0.011 * H, 0.008 * H, coat), + cap(fx - 0.008 * H + dangle(0), fy + 0.05 * H, fx - 0.006 * H + dangle(0.6), fy + 0.08 * H, 0.008 * H, 0.007 * H, coat), + ell(fx - 0.004 * H + dangle(0.8), fy + 0.084 * H, 0.009 * H, 0.007 * H, 0, coat), + cap(rx + 0.006 * H, ry + 0.014 * H, rx + 0.016 * H + dangle(0.3), ry + 0.05 * H, 0.014 * H, 0.009 * H, coat), + cap(rx + 0.016 * H + dangle(0.3), ry + 0.05 * H, rx + 0.012 * H + dangle(0.9), ry + 0.08 * H, 0.008 * H, 0.007 * H, coat), + ell(rx + 0.015 * H + dangle(1.1), ry + 0.084 * H, 0.009 * H, 0.007 * H, 0, coat), + ell(hx, hy - 0.005 * H, 0.024 * H, 0.021 * H, 0, coat), + ell(hx + 0.017 * H, hy + 0.003 * H, 0.012 * H, 0.009 * H, 0.2, pale), + ...[-0.012, 0.004].map((dx) => ell(hx + dx * H - lift * 0.002 * H, hy - (0.024 + lift * 0.004) * H, 0.006 * H, 0.006 * H, 0, coat)), ] // The tail hangs from the hips and sways, more at the tip. - let [tx, ty] = [rx - 0.02 * H, ry] + let [tx, ty] = [rx - 0.026 * H, ry] let angle = Math.PI / 2 + 0.25 * Math.sin(this.creatureTime * 0.8) for (let i = 0; i < 6; i++) { const nx = tx + Math.cos(angle) * 0.022 * H const ny = ty + Math.sin(angle) * 0.022 * H - parts.push(cap(tx, ty, nx, ny, 0.007 * H, 0.006 * H, i === 5 ? this.paint(BLACK) : coat)) - angle += 0.12 * Math.sin(this.creatureTime * 0.8 + i * 0.5) + parts.push(cap(tx, ty, nx, ny, lerp(0.009, 0.006, i / 6) * H, lerp(0.009, 0.006, (i + 1) / 6) * H, i === 5 ? this.paint(BLACK) : coat)) + angle += 0.12 * Math.sin(this.creatureTime * 0.8 - i * 0.5) ;[tx, ty] = [nx, ny] } this.shape(parts, this.lighting, 0.7) From 99698a1ab31281afc197ecd53d169b259b0cbae8 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:50:28 -0700 Subject: [PATCH 06/10] Shape the beach dolphins and crab, and walk the crab on jointed legs --- wallpapers/beach.ts | 72 +++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/wallpapers/beach.ts b/wallpapers/beach.ts index f977204..4b1b06d 100644 --- a/wallpapers/beach.ts +++ b/wallpapers/beach.ts @@ -1,4 +1,5 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" +import { limb, stride } from "../src/creature" 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" @@ -227,10 +228,10 @@ class Beach extends Canvas { } if (this.activity === "teeming") { const c = this.crab - c.phase += cdt * 10 if (c.rest > 0) c.rest -= cdt else { c.x += c.dir * 0.04 * cdt + c.phase += (0.04 * cdt) / 0.02 if (Math.random() < cdt * 0.3) c.rest = rand(2, 5) if (c.x > 0.75 * this.A) c.dir = -1 if (c.x < 0.3 * this.A) c.dir = 1 @@ -559,6 +560,8 @@ class Beach extends Canvas { } } + // A bottlenose dolphin: a tapering body with a rounded melon and short beak, a swept dorsal fin and flukes. Its tail + // trails the turn of the leap, flexing down as it rises and up as it dives. private drawDolphin(d: Dolphin) { if (d.t < 0) return const H = this.H @@ -568,21 +571,26 @@ class Beach extends Canvas { const angle = Math.atan2(-Math.cos(Math.PI * d.t) * 0.08 * Math.PI, d.dir * 0.16) const ca = Math.cos(angle) const sa = Math.sin(angle) - const P = (u: number, v: number): [number, number] => [x + (u * ca - v * sa) * S, y + (u * sa + v * ca) * S] + const f = Math.sign(ca || 1) + const bend = 0.35 * Math.cos(Math.PI * d.t) * f + const P = (u: number, v: number): [number, number] => { + const w = v + bend * Math.min(0, u) ** 2 + return [x + (u * ca - w * sa) * S, y + (u * sa + w * ca) * S] + } const back = this.paint([0.42, 0.52, 0.62]) - this.shape( - [ - cap(...P(-0.4, 0), ...P(0.35, 0), 0.08 * S, 0.11 * S, back), - cap(...P(0.35, 0.02), ...P(0.52, 0.04), 0.05 * S, 0.025 * S, back), - cap(...P(0, -0.1), ...P(-0.1 * Math.sign(ca || 1), -0.24), 0.05 * S, 0.01 * S, back), - cap(...P(-0.4, 0), ...P(-0.55, -0.1), 0.04 * S, 0.02 * S, back), - cap(...P(-0.4, 0), ...P(-0.55, 0.1), 0.04 * S, 0.02 * S, back), - cap(...P(0.1, 0.06), ...P(-0.02, 0.18), 0.035 * S, 0.01 * S, back), - ], - this.lighting, - 0.7, + const spine: [number, number][] = [[0.52, 0.018], [0.42, 0.03], [0.36, 0.07], [0.26, 0.1], [0.08, 0.115], [-0.12, 0.1], [-0.3, 0.06], [-0.46, 0.026]] + const parts: Part[] = spine.slice(1).map(([u, r], i) => cap(...P(spine[i][0], i < 2 ? 0.02 : 0), ...P(u, i < 1 ? 0.02 : 0), spine[i][1] * S, r * S, back)) + const beat = Math.sin(d.t * TAU * 1.5) * 0.04 + parts.push( + cap(...P(-0.46, 0), ...P(-0.62, -0.05 + beat), 0.03 * S, 0.012 * S, back), + cap(...P(-0.46, 0), ...P(-0.6, 0.06 + beat), 0.03 * S, 0.012 * S, back), + cap(...P(0.02, -0.1), ...P(-0.06, -0.22), 0.055 * S, 0.012 * S, back), + cap(...P(-0.06, -0.22), ...P(-0.12, -0.24), 0.012 * S, 0.006 * S, back), + cap(...P(0.18, 0.07), ...P(0.06, 0.17), 0.035 * S, 0.01 * S, back), ) - this.shape([cap(...P(-0.25, 0.04), ...P(0.3, 0.05), 0.04 * S, 0.06 * S, this.paint([0.75, 0.78, 0.8]))], this.lighting, 0.5) + this.shape(parts, this.lighting, 0.7) + this.shape([cap(...P(-0.2, 0.06), ...P(0.3, 0.065), 0.03 * S, 0.045 * S, this.paint([0.68, 0.72, 0.76]))], this.lighting, 0.5) + this.add(...P(0.36, -0.01), 0.02, 0.02, 0.02) } // Gulls wheeling over the water, wings flexing. @@ -642,27 +650,41 @@ class Beach extends Canvas { this.add(...P(-0.1, -0.1), this.look.light[0] * 0.5, this.look.light[1] * 0.5, this.look.light[2] * 0.5) } - // A red crab scuttling sideways across the sand, claws up, pausing now and then. + // A red crab scuttling sideways across the sand on jointed legs, four to a side stepping in two alternating sets, + // eyes up on stalks and claws raised; pausing, it waves a claw. private drawCrab() { const H = this.H const c = this.crab - const S = 0.05 * H - const x = c.x * H + const S = 0.065 * H + const going = c.rest > 0 ? 0 : 1 + const x = (c.x + Math.sin(c.phase * TAU * 2) * 0.006 * going) * H 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 - const parts: Part[] = [ell(x, y - 0.25 * S, 0.4 * S, 0.22 * S, 0, red)] + const dark = this.paint([0.55, 0.13, 0.07]) + const wave = (1 - going) * Math.max(0, Math.sin(this.creatureTime * 2.5)) ** 2 + const parts: Part[] = [] for (const side of [-1, 1]) - for (let k = 0; k < 3; k++) { - const kx = x + side * (0.25 + k * 0.1) * S - parts.push(cap(kx, y - 0.2 * S, kx + side * 0.25 * S, y + (k % 2 ? step : -step) * S, 0.035 * S, 0.025 * S, red)) + for (let k = 0; k < 4; k++) { + const [reach, lift] = stride(c.phase, ((k + (side > 0 ? 1 : 0)) % 2) * 0.5) + const hx = x + side * (0.2 + k * 0.05) * S + const fx = x + (side * (0.5 + k * 0.1) + reach * c.dir * 0.08 * going) * S + const fy = y - (lift * 0.08 * going + 0.02) * S + parts.push(...limb((u, v) => [u, v], hx, y - 0.25 * S, fx, fy, 0.22 * S, 0.28 * S, -side, 0.05 * S, 0.04 * S, 0.02 * S, k % 2 ? red : dark)) } + parts.push(ell(x, y - 0.3 * S, 0.38 * S, 0.2 * S, 0, red)) for (const side of [-1, 1]) { - parts.push(cap(x + side * 0.3 * S, y - 0.35 * S, x + side * 0.45 * S, y - 0.6 * S, 0.04 * S, 0.035 * S, red)) - parts.push(ell(x + side * 0.48 * S, y - 0.68 * S, 0.12 * S, 0.09 * S, side * 0.5, red)) + parts.push(cap(x + side * 0.08 * S, y - 0.42 * S, x + side * 0.1 * S, y - 0.56 * S, 0.025 * S, 0.02 * S, red)) + const lift = side > 0 ? wave * 0.25 : 0 + const [cx, cy] = [x + side * 0.5 * S, y - (0.66 + lift) * S] + parts.push( + cap(x + side * 0.28 * S, y - 0.34 * S, x + side * 0.4 * S, y - (0.5 + lift * 0.6) * S, 0.045 * S, 0.035 * S, red), + ell(cx, cy, 0.13 * S, 0.1 * S, side * 0.5, red), + cap(cx + side * 0.04 * S, cy - 0.06 * S, cx + side * 0.06 * S, cy - (0.2 + wave * 0.05) * S, 0.05 * S, 0.012 * S, red), + cap(cx + side * 0.09 * S, cy - 0.02 * S, cx + side * 0.16 * S, cy - 0.14 * S, 0.035 * S, 0.01 * S, red), + ) } this.shape(parts, this.lighting, 0.6) - for (const side of [-1, 1]) this.add(x + side * 0.1 * S, y - 0.45 * S, 0.02, 0.02, 0.02) + for (const side of [-1, 1]) this.disc(x + side * 0.1 * S, y - 0.58 * S, 0.03 * S, 0.02, 0.02, 0.02, 1) } // A surfer standing on a board, riding the swell along the shore and bobbing. From 152f44465f82f729ce38ba1b60da2073b96b8557 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:52:13 -0700 Subject: [PATCH 07/10] Give the zen heron jointed, wading legs and trailing wingtips and crest, at koi scale --- wallpapers/zen.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/wallpapers/zen.ts b/wallpapers/zen.ts index c1f2340..2997e52 100644 --- a/wallpapers/zen.ts +++ b/wallpapers/zen.ts @@ -1,4 +1,5 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" +import { limb } from "../src/creature" 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" @@ -1001,11 +1002,16 @@ class Zen extends Canvas { : h.phase === "in" ? [lerp(-0.25, sx, 1 - (1 - p) ** 2), lerp(0.22, sy, 1 - (1 - p) ** 2), clamp((1 - p) / 0.25, 0, 1)] : [lerp(sx, this.A + 0.3, p * p), lerp(sy, 0.18, p * p), clamp(p / 0.12, 0, 1)] - const S = 0.2 * H + const S = 0.16 * H const P = (u: number, v: number): [number, number] => [x * H + u * S, y * H + v * S] const L = (a: [number, number], b: [number, number]): [number, number] => [lerp(a[0], b[0], fly), lerp(a[1], b[1], fly)] - const hunt = h.phase === "stand" ? smoothstep(0.2, 0.9, 0.5 - 0.5 * Math.cos(h.t * 0.45)) : 0 + const huntAt = (t: number) => (h.phase === "stand" ? smoothstep(0.2, 0.9, 0.5 - 0.5 * Math.cos(t * 0.45)) : 0) + const hunt = huntAt(h.t) + // The crest plume trails the head as it leans out and back. + const sweep = (huntAt(h.t - 0.5) - hunt) * 0.6 + // The wingtips beat a moment behind the arm. const flap = Math.sin(this.time * 2.6) + const lag = Math.sin(this.time * 2.6 - 0.7) const body = this.paint([0.62, 0.66, 0.72]) const neck = this.paint([0.82, 0.82, 0.84]) const wing = this.paint([0.38, 0.42, 0.5]) @@ -1015,13 +1021,20 @@ class Zen extends Canvas { const n2 = L([lerp(0.09, 0.3, hunt), lerp(-0.7, -0.56, hunt)], [0.16, -0.49]) const head = L([lerp(0.14, 0.38, hunt), lerp(-0.8, -0.52, hunt)], [0.22, -0.47]) const beak = L([lerp(0.15, 0.1, hunt), lerp(0.01, 0.11, hunt)], [0.15, 0.015]) - const wingTip: [number, number] = [-0.08, -0.45 - 0.42 * flap] + const wingTip: [number, number] = [-0.08, -0.45 - 0.42 * lag] const wingMid: [number, number] = [-0.03, -0.45 - 0.22 * flap] + // Wading, it now and then lifts a foot and sets it down a little further on; the ankles bend back. + const wade = (side: number) => (h.phase === "stand" ? Math.max(0, Math.sin(h.t * 0.3 + side * 1.6)) ** 10 : 0) + const leg = (side: number): Part[] => { + const hip = L([side * 0.015, -0.33], [-0.1, -0.4 + side * 0.01]) + const foot = L([side * 0.025 + wade(side) * 0.04, -wade(side) * 0.09], [-0.4, -0.37 + side * 0.02]) + return [...limb(P, ...hip, ...foot, 0.17, 0.17, 1, 0.012 * S, 0.01 * S, 0.008 * S, legs), cap(...P(...foot), ...P(foot[0] + L([0.06, 0], [-0.04, 0])[0], foot[1]), 0.006 * S, 0.004 * S, legs)] + } if (fly > 0.3) this.shape([cap(...P(0.03, -0.45), ...P(wingMid[0] + 0.04, wingMid[1] + 0.02), 0.07 * S, 0.05 * S, this.paint([0.28, 0.31, 0.38])), cap(...P(wingMid[0] + 0.04, wingMid[1] + 0.02), ...P(wingTip[0] + 0.05, wingTip[1] + 0.03), 0.05 * S, 0.02 * S, this.paint([0.28, 0.31, 0.38]))], this.lighting, 0.4) this.shape( [ - cap(...P(...L([-0.02, 0], [-0.4, -0.37])), ...P(...L([-0.01, -0.32], [-0.1, -0.4])), 0.011 * S, 0.012 * S, legs), - cap(...P(...L([0.03, 0], [-0.42, -0.4])), ...P(...L([0.01, -0.32], [-0.1, -0.41])), 0.011 * S, 0.012 * S, legs), + ...leg(-1), + ...leg(1), ell(...P(0, L([0, -0.41], [0, -0.42])[1]), 0.17 * S, 0.075 * S, -0.4 * (1 - fly), body), cap(...P(-0.12, L([-0.34, -0.33], [-0.42, -0.42])[1]), ...P(-0.2, L([-0.3, -0.3], [-0.42, -0.42])[1]), 0.04 * S, 0.02 * S, wing), cap(...P(...shoulder), ...P(...n1), 0.042 * S, 0.032 * S, neck), @@ -1029,12 +1042,12 @@ class Zen extends Canvas { cap(...P(...n2), ...P(...head), 0.026 * S, 0.03 * S, neck), ell(...P(...head), 0.042 * S, 0.032 * S, 0, neck), cap(...P(...head), ...P(head[0] + beak[0], head[1] + beak[1]), 0.016 * S, 0.003 * S, this.paint([0.9, 0.72, 0.2])), - cap(...P(head[0] - 0.01, head[1] - 0.02), ...P(head[0] - 0.12, head[1] - 0.01), 0.009 * S, 0.003 * S, this.paint([0.08, 0.08, 0.1])), + cap(...P(head[0] - 0.01, head[1] - 0.02), ...P(head[0] - 0.12 - sweep * 0.3, head[1] - 0.01 - sweep * 0.15), 0.009 * S, 0.003 * S, this.paint([0.08, 0.08, 0.1])), ], this.lighting, 0.6, ) - if (fly > 0.3) this.shape([cap(...P(0.03, -0.44), ...P(...wingMid), 0.08 * S, 0.06 * S, wing), cap(...P(...wingMid), ...P(...wingTip), 0.06 * S, 0.025 * S, wing), cap(...P(...wingTip), ...P(wingTip[0] - 0.08, wingTip[1] + 0.02), 0.03 * S, 0.012 * S, this.paint([0.15, 0.16, 0.2]))], this.lighting, 0.6) + if (fly > 0.3) this.shape([cap(...P(0.03, -0.44), ...P(...wingMid), 0.08 * S, 0.06 * S, wing), cap(...P(...wingMid), ...P(...wingTip), 0.06 * S, 0.025 * S, wing), cap(...P(...wingTip), ...P(wingTip[0] - 0.08, wingTip[1] + 0.02 + (lag - flap) * 0.1), 0.03 * S, 0.012 * S, this.paint([0.15, 0.16, 0.2]))], this.lighting, 0.6) if (fly === 0) this.add(...P(head[0] + 0.02, head[1] - 0.005), 0.25, 0.2, 0.02) } From 5d45e9f7f04e1ddd5a4e7ef1ef6944b3ecf6928a Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:53:57 -0700 Subject: [PATCH 08/10] Walk city pedestrians with bending knees, planted steps and swinging arms --- wallpapers/city.ts | 47 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/wallpapers/city.ts b/wallpapers/city.ts index c2fb6c8..76b2146 100644 --- a/wallpapers/city.ts +++ b/wallpapers/city.ts @@ -1,4 +1,5 @@ import { Canvas, cap, ell, type Lighting, type Part } from "../src/canvas" +import { bob, limb, stride } from "../src/creature" 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" @@ -140,6 +141,12 @@ const PAINT: RGB[] = [ [0.3, 0.55, 0.4], [0.55, 0.56, 0.6], ] +const HAIR: RGB[] = [ + [0.12, 0.08, 0.05], + [0.45, 0.28, 0.12], + [0.05, 0.05, 0.06], + [0.7, 0.55, 0.3], +] const NEON: RGB[] = [ [1.5, 0.2, 0.95], [0.2, 1.3, 1.25], @@ -188,8 +195,11 @@ interface Car { interface Walker { x: number dir: number + // Strides taken: one per step of both feet. phase: number + size: number coat: RGB + hair: RGB umbrella: RGB } @@ -254,7 +264,7 @@ class City extends Canvas { 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]] - this.walkers = Array.from({ length: 5 }, (_, i) => ({ x: 0.15 + i * 0.37, dir: i % 2 ? -1 : 1, phase: Math.random() * TAU, coat: PAINT[(i * 3) % PAINT.length], umbrella: umbrellas[i] })) + this.walkers = Array.from({ length: 5 }, (_, i) => ({ x: 0.15 + i * 0.37, dir: i % 2 ? -1 : 1, phase: Math.random(), size: [1, 0.92, 1.06, 0.97, 1.03][i], coat: PAINT[(i * 3) % PAINT.length], hair: HAIR[i % HAIR.length], umbrella: umbrellas[i] })) if (this.look.night && !settings.weather) this.rain = Array.from({ length: 30 }, () => ({ x: Math.random() * 2, y: Math.random(), speed: rand(0.9, 1.3) })) } @@ -277,7 +287,7 @@ class City extends Canvas { if (b.x < -0.15) b.x = this.A + 0.15 } for (const w of this.walkers) { - w.phase += cdt * 6 + w.phase += (0.06 * cdt) / (0.07 * w.size) w.x += w.dir * 0.06 * cdt if (w.x > this.A + 0.05) w.x = -0.05 if (w.x < -0.05) w.x = this.A + 0.05 @@ -946,24 +956,37 @@ class City extends Canvas { if (lit) this.glow(this.hdr, X(L / 2), y - 0.006 * H, 0.004 * H, 0.004 * H, HEADLIGHT, 1.5 * look.glow) } - // People strolling along the sidewalk, under umbrellas when it rains. + // People strolling along the sidewalk, under umbrellas when it rains. Each step lands and holds, knees bending, + // arms swinging against the legs and the body rising over each stride. private drawWalker(w: Walker) { const { H, look } = this - const S = 0.1 * H - const x = w.x * H - const y = 0.996 * H - const P = (u: number, v: number): [number, number] => [x + u * w.dir * S, y + v * S] - const swing = Math.sin(w.phase) * 0.12 + const S = 0.1 * H * w.size + const rise = bob(w.phase) * 0.015 + const P = (u: number, v: number): [number, number] => [w.x * H + u * w.dir * S, 0.996 * H + (v - rise) * S] const coat = this.paint(w.coat) const legs = this.paint([0.15, 0.15, 0.2]) const skin = this.paint([0.8, 0.6, 0.48]) + const shoe = this.paint([0.08, 0.07, 0.07]) + const side = (s: number, far: boolean): Part[] => { + const [reach, lift] = stride(w.phase, s > 0 ? 0 : 0.5) + const fu = reach * 0.21 + const fv = -lift * 0.07 - 0.025 + rise + const umbrella = this.wet && !far + const hand: [number, number] = umbrella ? [0.12, -0.85] : [-reach * 0.13 + 0.02, -0.44] + const shade = (c: RGB): RGB => (far ? [c[0] * 0.7, c[1] * 0.7, c[2] * 0.7] : c) + return [ + ...limb(P, 0, -0.47, fu, fv, 0.24, 0.24, -1, 0.045 * S, 0.037 * S, 0.032 * S, shade(legs)), + cap(...P(fu - 0.02, fv), ...P(fu + 0.06, fv + 0.006), 0.028 * S, 0.024 * S, shoe), + ...limb(P, 0.01, -0.77, ...hand, 0.18, 0.18, umbrella ? -1 : 1, 0.033 * S, 0.028 * S, 0.025 * S, shade(coat)), + ] + } + this.shape(side(-1, true), this.lighting, 0.6) this.shape( [ - cap(...P(0, -0.45), ...P(-swing, 0), 0.04 * S, 0.035 * S, legs), - cap(...P(0, -0.45), ...P(swing, 0), 0.04 * S, 0.035 * S, legs), + ...side(1, false), cap(...P(0, -0.42), ...P(0.01, -0.8), 0.085 * S, 0.075 * S, coat), - cap(...P(0.01, -0.76), ...P(swing * 0.5 + (this.wet ? 0.12 : 0), this.wet ? -0.85 : -0.45), 0.03 * S, 0.026 * S, coat), - ell(...P(0.02, -0.89), 0.06 * S, 0.068 * S, 0, skin), + ell(...P(0.02, -0.89 + Math.cos(TAU * 2 * w.phase - 1) * 0.008), 0.06 * S, 0.068 * S, 0, skin), + ell(...P(0.005, -0.925 + Math.cos(TAU * 2 * w.phase - 1) * 0.008), 0.062 * S, 0.04 * S, -0.3 * w.dir, this.paint(w.hair)), ], this.lighting, 0.6, From 35cc1905525770da97a34603cc17297d7a23fbfb Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 17:56:40 -0700 Subject: [PATCH 09/10] Bring the ocean's reef fish down to scale and let the turtle's flipper tips trail the stroke --- wallpapers/ocean.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wallpapers/ocean.ts b/wallpapers/ocean.ts index ec5e136..7618d6c 100644 --- a/wallpapers/ocean.ts +++ b/wallpapers/ocean.ts @@ -144,7 +144,7 @@ class Ocean extends Canvas { this.timeOfDay = settings.time if (activity === "teeming") this.fish.push(this.makeFish("ambient:shark", "shark", [0.3, 1, 0.17, 0.33], { len: 0.64, z: 0.55 })) if (activity !== "calm") { - this.fish.push(this.makeFish("ambient:fish", "fish", [0, 0.44, 0.48, 0.72], { len: 0.26, z: 0.35, hue: 0.07, sat: 0.85 })) + this.fish.push(this.makeFish("ambient:fish", "fish", [0, 0.44, 0.48, 0.72], { len: 0.19, z: 0.35, hue: 0.07, sat: 0.85 })) const zone: Zone = [0.58, 1, 0.46, 0.7] const ox = ((zone[0] + zone[1]) / 2) * this.A this.octopus = { x: ox, y: 0.58, z: 0.4, vx: 0, vy: 0, size: 0.1, hue: 0.02, phase: 0, pulse: 0, next: 2, dir: -Math.PI / 2, wx: ox, wy: 0.58, zone } @@ -1162,7 +1162,9 @@ class Ocean extends Canvas { const skinFar: RGB = [0.26, 0.3, 0.2] const stroke = Math.sin(tu.phase) - // A flipper: a curved paddle from its root, swept back and rotated by the stroke. + // A flipper: a curved paddle from its root, swept back and rotated by the stroke. Its tip trails the stroke, + // curling one way on the downstroke and the other on the way back up. + const whip = Math.cos(tu.phase) * 0.05 const flipper = (u0: number, v0: number, length: number, angle: number, width: number, color: RGB) => { const [r, g, b] = tint(color) let u = u0 @@ -1176,7 +1178,7 @@ class Ocean extends Canvas { blob(x, y, Math.max(0.6, w * L), r, g, b) u += Math.cos(a) * (length / n) v += Math.sin(a) * (length / n) - a += 0.04 + a += 0.04 + whip * q } } const front = Math.PI - 0.55 - stroke * 0.6 From 5fd799813a07bbfc0f0f7e3670bede1b7009d945 Mon Sep 17 00:00:00 2001 From: vprdev Date: Fri, 9 Oct 2026 21:02:19 -0700 Subject: [PATCH 10/10] Document the creature helpers and export them from the API --- CONTRIBUTING.md | 9 ++++++++- src/api.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c067710..a4c895a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,13 @@ details), and values in between soften it. - `makeClouds(count, puffy, y0, y1)`, `driftClouds(clouds, A, dt)` and `paintClouds(hdr, W, H, clouds, top, bottom, alpha, puffy)` give puffy cumulus or thin streaks, shaded from `top` to `bottom`. +`src/creature.ts` animates walkers. Pose a creature in body coordinates with `body(x, y, S, turn)` (u forward, v +down, w across), and keep a `Gait` per walker: `stepGait(g, distance / stride, dt, face)` advances the step cycle with +the distance walked, so planted feet don't slide, eases between standing and walking, turns through a three-quarter +view instead of flipping, and rings a springy `lag` for tails and ears to trail. `quadruped()` draws four jointed +legs in diagonal pairs, `limb()` any two-segment limb with a bending joint, `stride()` and `bob()` one foot's step and +the body's rise, and `chain()` a tapered tail or neck along a curve. + `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. @@ -272,7 +279,7 @@ bun dev/gif.ts all # re-renders the README's GIFs ## Your own package -A wallpaper package imports the API from `opencode-wallpapers/api`, which has `Canvas`, `cap`, `ell`, the sky, +A wallpaper package imports the API from `opencode-wallpapers/api`, which has `Canvas`, `cap`, `ell`, the creature, sky, weather, flock, egg and math helpers, and the `Wallpaper` types, with no OpenCode or OpenTUI dependency. It isn't on npm yet, so build a clone of this repository and depend on it by path: diff --git a/src/api.ts b/src/api.ts index 1815efa..62e5150 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,6 @@ // The public API for wallpapers, imported as "opencode-wallpapers/api". Nothing here depends on OpenCode or OpenTUI. export { Canvas, cap, ell, type Lighting, type Part } from "./canvas" +export { bob, body, chain, gait, joint, limb, quadruped, stepGait, stride, view, type Body, type Gait } from "./creature" 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"