Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,13 @@ details), and values in between soften it.
of height), `core` and `glow` colors, `near` and `wide` glow strengths, and `moon: true` for a cratered moon.
- `makeStars(count, top)` and `paintStars(hdr, W, H, stars, time, top, brightness)` draw slowly twinkling stars that
fade toward `top`. Stars are tinted from `STAR_TINTS`, never white.
- `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`.
- `makeClouds(count, kind, y0, y1)`, `driftClouds(clouds, A, dt)` and `paintClouds(hdr, W, H, clouds, top, bottom,
alpha, orb)` give clouds of one kind: `cumulus`, `towering` (standing on y, for a horizon), `cirrus`, `wisp`,
`stratus` or `ash`. They are lit from the orb: the brighter of `top` and `bottom` faces it, and clouds near it are
backlit with glowing edges. Pass no orb under an overcast deck to light them evenly from above. Each cloud's shape
is built once per screen size, so drawing them each frame is cheap.
- `makeShadows(count, y0, y1)` and `paintShadows(hdr, W, H, shadows, ground, orb)` drift soft cloud shadows across
the ground below `ground` under a high sun; `paintHaze(hdr, W, H, y0, y1, color, alpha)` paints a static haze band.

`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.
Expand Down
2 changes: 1 addition & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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 { STAR_TINTS, driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "./sky"
export { STAR_TINTS, driftClouds, makeClouds, makeShadows, makeStars, makeStorm, paintClouds, paintHaze, paintShadows, paintSky, paintStars, paintStorm, type Cloud, type CloudKind, type Orb, type Star } from "./sky"
export {
ACTIVITIES,
AGENT_EVENTS,
Expand Down
349 changes: 305 additions & 44 deletions src/sky.ts

Large diffs are not rendered by default.

21 changes: 14 additions & 7 deletions wallpapers/beach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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 { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky"
import { driftClouds, makeClouds, makeShadows, makeStars, makeStorm, paintClouds, paintShadows, 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"

Expand All @@ -22,7 +22,7 @@ interface Look {
sky: [number, RGB][]
orb: Orb
stars: number
clouds: { puffy: boolean; top: RGB; bottom: RGB; alpha: number }
clouds: { top: RGB; bottom: RGB; alpha: number }
sea: [far: RGB, near: RGB]
sand: [dry: RGB, wet: RGB]
foam: RGB
Expand All @@ -42,7 +42,7 @@ const LOOKS: Record<Time, Look> = {
],
orb: { x: 0.18, y: 0.14, r: 0.035, core: [5, 4.6, 3.8], glow: [1, 0.95, 0.8], near: 0.45, wide: 0.12 },
stars: 0,
clouds: { puffy: true, top: [1.1, 1.1, 1.1], bottom: [0.6, 0.68, 0.8], alpha: 0.9 },
clouds: { top: [1.1, 1.1, 1.1], bottom: [0.6, 0.68, 0.8], alpha: 0.9 },
sea: [
[0.12, 0.42, 0.62],
[0.08, 0.6, 0.62],
Expand All @@ -67,7 +67,7 @@ const LOOKS: Record<Time, Look> = {
],
orb: { x: 0.55, y: 0.475, r: 0.055, core: [3.2, 1.7, 0.6], glow: [1, 0.42, 0.14], near: 0.5, wide: 0.25 },
stars: 25,
clouds: { puffy: false, top: [0.1, 0.035, 0.09], bottom: [0.8, 0.3, 0.2], alpha: 0.55 },
clouds: { top: [0.1, 0.035, 0.09], bottom: [0.8, 0.3, 0.2], alpha: 0.8 },
sea: [
[0.55, 0.25, 0.18],
[0.12, 0.07, 0.12],
Expand All @@ -91,7 +91,7 @@ const LOOKS: Record<Time, Look> = {
],
orb: { x: 0.7, y: 0.16, r: 0.03, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 140,
clouds: { puffy: false, top: [0.015, 0.02, 0.05], bottom: [0.06, 0.1, 0.3], alpha: 0.4 },
clouds: { top: [0.015, 0.02, 0.05], bottom: [0.06, 0.1, 0.3], alpha: 0.7 },
sea: [
[0.03, 0.06, 0.15],
[0.01, 0.03, 0.07],
Expand Down Expand Up @@ -166,6 +166,9 @@ class Beach extends Canvas {
private background = new Float32Array(0)
private stars: Star[]
private clouds: Cloud[]
// Towering cumulus far out on the sea horizon, painted into the background, and shadows of the near clouds.
private towers = makeClouds(3, "towering", HORIZON, HORIZON)
private shadows = makeShadows(3, HORIZON + 0.06, 0.95)
private storm = makeStorm()
private palms: Palm[] = []
private boat = { x: -9, dir: 1, next: 14 }
Expand All @@ -192,7 +195,7 @@ class Beach extends Canvas {
this.look = this.weather.covered ? { ...look, glitter: look.glitter * 0.15 } : look
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)
this.clouds = makeClouds(4, "cumulus", 0.07, 0.24)
if (settings.activity !== "calm") this.dolphins = [0, 1].map((i) => ({ t: -1, x: 0, y: 0, dir: 1, wait: 4 + i * 9 }))
}

Expand All @@ -202,6 +205,7 @@ class Beach extends Canvas {
const cdt = dt * CREATURE_SPEED
this.creatureTime += cdt
driftClouds(this.clouds, this.A, dt)
driftClouds(this.shadows, this.A, dt)
driftClouds(this.storm, this.A, dt)
this.stepGloom(dt)
this.weather.step(dt)
Expand Down Expand Up @@ -250,9 +254,11 @@ class Beach extends Canvas {
this.hdr.set(this.background)
if (!this.weather.covered) paintStars(this.hdr, this.W, this.H, this.stars, this.time, 0.45, this.look.stars > 50 ? 0.5 : 0.3)
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)
const orb = this.weather.covered ? undefined : this.look.orb
paintClouds(this.hdr, this.W, this.H, this.clouds, top, bottom, this.look.clouds.alpha, orb)
paintStorm(this.hdr, this.W, this.H, this.storm, this.look.clouds.top, this.look.clouds.bottom, this.gloom)
this.drawSea()
paintShadows(this.hdr, this.W, this.H, this.shadows, HORIZON + 0.01, orb)
for (const s of this.swimmers) this.drawSwimmer(s)
this.drawBoat()
for (const d of this.dolphins) this.drawDolphin(d)
Expand Down Expand Up @@ -282,6 +288,7 @@ class Beach extends Canvas {
this.lighting = look.style === "front" ? { style: "front", dir: DAYLIGHT } : { style: "rim", color: look.light, x: look.orb.x * W, y: look.orb.y * H }
paintSky(this.hdr, W, H, look.sky, look.orb)
this.weather.cover(this.hdr, W, H)
if (!this.weather.covered) paintClouds(this.hdr, W, H, this.towers, look.clouds.top, look.clouds.bottom, look.clouds.alpha, look.orb)
// A small island on the horizon.
const ix = 0.16 * A
this.polygon(Array.from({ length: 21 }, (_, i) => [(ix + Math.cos((i / 20) * Math.PI) * 0.08) * H, (HORIZON - Math.sin((i / 20) * Math.PI) * 0.025) * H] as const), this.paint([0.2, 0.35, 0.22]))
Expand Down
18 changes: 11 additions & 7 deletions wallpapers/city.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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 { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky"
import { driftClouds, makeClouds, makeStars, makeStorm, paintClouds, paintHaze, paintSky, paintStars, paintStorm, type Cloud, type Orb, type Star } from "../src/sky"
import type { Activity, Settings, Time, Wallpaper } from "../src/wallpaper"
import { WeatherLayer } from "../src/weather"

Expand All @@ -27,7 +27,7 @@ interface Look {
sky: [number, RGB][]
orb: Orb
stars: number
clouds: { puffy: boolean; top: RGB; bottom: RGB; alpha: number }
clouds: { top: RGB; bottom: RGB; alpha: number }
// The far skyline's hazy color and the river from far to near.
haze: RGB
river: [far: RGB, near: RGB]
Expand All @@ -54,7 +54,7 @@ const LOOKS: Record<Time, Look> = {
],
orb: { x: 0.14, y: 0.11, r: 0.035, core: [5, 4.6, 3.8], glow: [1, 0.95, 0.8], near: 0.45, wide: 0.12 },
stars: 0,
clouds: { puffy: true, top: [1.1, 1.1, 1.1], bottom: [0.6, 0.66, 0.78], alpha: 0.85 },
clouds: { top: [1.1, 1.1, 1.1], bottom: [0.6, 0.66, 0.78], alpha: 0.85 },
haze: [0.55, 0.65, 0.78],
river: [
[0.2, 0.36, 0.48],
Expand All @@ -78,7 +78,7 @@ const LOOKS: Record<Time, Look> = {
],
orb: { x: 0.7, y: 0.5, r: 0.055, core: [3.2, 1.7, 0.6], glow: [1, 0.42, 0.14], near: 0.5, wide: 0.25 },
stars: 20,
clouds: { puffy: false, top: [0.1, 0.035, 0.09], bottom: [0.8, 0.3, 0.2], alpha: 0.55 },
clouds: { top: [0.1, 0.035, 0.09], bottom: [0.8, 0.3, 0.2], alpha: 0.55 },
haze: [0.36, 0.14, 0.16],
river: [
[0.4, 0.16, 0.14],
Expand All @@ -101,7 +101,7 @@ const LOOKS: Record<Time, Look> = {
],
orb: { x: 0.82, y: 0.12, r: 0.03, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 50,
clouds: { puffy: false, top: [0.015, 0.02, 0.05], bottom: [0.08, 0.05, 0.2], alpha: 0.4 },
clouds: { top: [0.015, 0.02, 0.05], bottom: [0.08, 0.05, 0.2], alpha: 0.4 },
haze: [0.035, 0.03, 0.08],
river: [
[0.03, 0.025, 0.08],
Expand Down Expand Up @@ -249,7 +249,7 @@ class City extends Canvas {
this.activity = settings.activity
this.look = LOOKS[settings.time]
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.clouds = makeClouds(2, "stratus", 0.05, 0.12)
this.weather = new WeatherLayer(settings.weather ?? "clear", settings.time, BANK)
this.wet = settings.weather ? settings.weather === "rain" || settings.weather === "snow" : this.look.night && settings.activity === "teeming"
if (settings.activity !== "teeming") return
Expand Down Expand Up @@ -296,7 +296,7 @@ class City extends Canvas {
this.hdr.set(this.background)
if (!this.weather.covered) paintStars(this.hdr, W, H, this.visibleStars, this.time, BANK, 0.4)
const [top, bottom] = this.weather.scud ?? [look.clouds.top, look.clouds.bottom]
paintClouds(this.hdr, W, H, this.clouds, top, bottom, look.clouds.alpha, look.clouds.puffy)
paintClouds(this.hdr, W, H, this.clouds, top, bottom, look.clouds.alpha, this.weather.covered ? undefined : look.orb)
paintStorm(this.hdr, W, H, this.storm, look.clouds.top, look.clouds.bottom, this.gloom)
if (this.hero.t >= 0) this.drawHero()
this.drawBlimp()
Expand Down Expand Up @@ -341,6 +341,9 @@ class City extends Canvas {
this.paintTower(this.tower(x, w, 0.06 + hash(i * 3.1 + 0.7) ** 1.5 * 0.17, i + 500, true))
x += w * (0.7 + hash(i * 5.3) * 0.4)
}
// City haze glowing with the horizon's light over the far towers, which the downtown skyline stands in front of.
const horizon = look.sky[look.sky.length - 1][1]
paintHaze(this.hdr, W, H, BANK - 0.3, BANK, horizon, 0.7)
const near: Tower[] = []
for (let x = -0.02, i = 0; x < A + 0.02; i++) {
const w = 0.05 + hash(i * 2.3 + 0.1) * 0.055
Expand All @@ -353,6 +356,7 @@ class City extends Canvas {
tallest[0].crown = 3
tallest[1].crown = 2
for (const t of near) this.paintTower(t)
paintHaze(this.hdr, W, H, BANK - 0.1, BANK, horizon, 0.25)
this.visibleStars = this.stars.filter((s) => {
const o = (Math.floor(s.y * H) * W + Math.floor(s.x * W)) * 3
return this.hdr[o] === sky[o] && this.hdr[o + 1] === sky[o + 1] && this.hdr[o + 2] === sky[o + 2]
Expand Down
62 changes: 9 additions & 53 deletions wallpapers/desert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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 { driftClouds, makeStorm, paintStorm } from "../src/sky"
import { driftClouds, makeClouds, makeStorm, paintClouds, paintStorm, type Cloud } from "../src/sky"
import type { Activity, Season, Settings, Time, Wallpaper } from "../src/wallpaper"
import { WeatherLayer } from "../src/weather"

Expand All @@ -23,7 +23,7 @@ interface Look {
orb: { x: number; y: number; r: number; core: RGB; glow: RGB; near: number; wide: number; moon: boolean }
stars: number
milkyWay: boolean
clouds: { puffy: boolean; top: RGB; bottom: RGB; alpha: number }
clouds: { top: RGB; bottom: RGB; alpha: number }
farMesa: RGB
nearMesa: RGB
floor: [RGB, RGB]
Expand Down Expand Up @@ -62,7 +62,7 @@ const LOOKS: Record<Time, Look> = {
orb: { x: 0.78, y: 0.13, r: 0.035, core: [5, 4.6, 3.8], glow: [1, 0.95, 0.8], near: 0.45, wide: 0.15, moon: false },
stars: 0,
milkyWay: false,
clouds: { puffy: true, top: [1.1, 1.1, 1.1], bottom: [0.55, 0.62, 0.75], alpha: 0.9 },
clouds: { top: [1.1, 1.1, 1.1], bottom: [0.55, 0.62, 0.75], alpha: 0.9 },
farMesa: [0.62, 0.36, 0.26],
nearMesa: [0.62, 0.27, 0.13],
floor: [
Expand Down Expand Up @@ -106,7 +106,7 @@ const LOOKS: Record<Time, Look> = {
orb: { x: 0.6, y: 0.53, r: 0.065, core: [3.2, 1.7, 0.6], glow: [1, 0.42, 0.14], near: 0.5, wide: 0.25, moon: false },
stars: 45,
milkyWay: false,
clouds: { puffy: false, top: [0.1, 0.035, 0.09], bottom: [0.75, 0.24, 0.2], alpha: 0.55 },
clouds: { top: [0.1, 0.035, 0.09], bottom: [0.75, 0.24, 0.2], alpha: 0.55 },
farMesa: [0.07, 0.03, 0.06],
nearMesa: [0.07, 0.025, 0.04],
floor: [
Expand Down Expand Up @@ -149,7 +149,7 @@ const LOOKS: Record<Time, Look> = {
orb: { x: 0.3, y: 0.16, r: 0.035, core: [1, 0.72, 0.3], glow: [0.45, 0.3, 0.12], near: 0.2, wide: 0.08, moon: true },
stars: 160,
milkyWay: true,
clouds: { puffy: false, top: [0.015, 0.02, 0.05], bottom: [0.06, 0.1, 0.3], alpha: 0.5 },
clouds: { top: [0.015, 0.02, 0.05], bottom: [0.06, 0.1, 0.3], alpha: 0.5 },
farMesa: [0.03, 0.04, 0.08],
nearMesa: [0.012, 0.016, 0.035],
floor: [
Expand Down Expand Up @@ -214,13 +214,6 @@ interface Mesa {
slope: number
}

interface Cloud {
x: number
y: number
speed: number
puffs: { dx: number; dy: number; rx: number; ry: number }[]
}

interface Bird {
angle: number
speed: number
Expand Down Expand Up @@ -290,14 +283,7 @@ class Desert extends Canvas {
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)] }))
this.clouds = Array.from({ length: look.clouds.puffy ? 4 : 5 }, () => ({
x: Math.random() * 2,
y: look.clouds.puffy ? 0.1 + Math.random() * 0.2 : 0.12 + Math.random() * 0.24,
speed: 0.003 + Math.random() * 0.004,
puffs: look.clouds.puffy
? Array.from({ length: 6 }, (_, i) => ({ dx: (i / 5 - 0.5) * 0.16 + (Math.random() - 0.5) * 0.03, dy: -Math.sin((i / 5) * Math.PI) * 0.02 + (Math.random() - 0.5) * 0.01, rx: 0.03 + Math.random() * 0.025, ry: 0.022 + Math.random() * 0.014 }))
: Array.from({ length: 5 }, () => ({ dx: (Math.random() - 0.5) * 0.22, dy: (Math.random() - 0.5) * 0.015, rx: 0.06 + Math.random() * 0.08, ry: 0.008 + Math.random() * 0.01 })),
}))
this.clouds = makeClouds(5, "cirrus", 0.08, 0.32)
if (settings.activity !== "calm") {
this.bigBird = { angle: 0, speed: 0.5, cx: 0.55, cy: 0.2, rx: 0.3, ry: 0.05, phase: 0 }
this.snake = { x: 0.5, y: 0.9, vx: 0, vy: 0, wx: 0.5, wy: 0.9, wanderT: 0, phase: 0, trail: [] }
Expand All @@ -318,10 +304,7 @@ class Desert extends Canvas {
d.y += Math.sin(this.time * 0.5 + d.s * 30) * 0.002 * dt
if (d.x > this.A) d.x -= this.A
}
for (const c of this.clouds) {
c.x += c.speed * dt
if (c.x > this.A + 0.4) c.x = -0.4
}
driftClouds(this.clouds, this.A, dt)
driftClouds(this.storm, this.A, dt)
this.stepGloom(dt)
this.stepTumbleweed(dt)
Expand All @@ -340,7 +323,8 @@ class Desert extends Canvas {
render() {
this.hdr.set(this.background)
if (!this.weather.covered) this.drawStars()
this.drawClouds()
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.weather.covered ? undefined : this.look.orb)
paintStorm(this.hdr, this.W, this.H, this.storm, this.look.clouds.top, this.look.clouds.bottom, this.gloom)
for (const b of this.flock) this.look.night ? this.drawBat(b) : this.drawBird(b, 0.3, "vulture")
if (this.bigBird) this.drawBird(this.bigBird, 0.8, this.look.night ? "owl" : "eagle")
Expand Down Expand Up @@ -732,34 +716,6 @@ class Desert extends Canvas {
}
}

// Clouds drifting slowly across: puffy and sunlit from above by day, thin streaks lit from below otherwise.
private drawClouds() {
const { H, W, hdr, look } = this
const { alpha, puffy } = look.clouds
const [top, bottom] = this.weather.scud ?? [look.clouds.top, look.clouds.bottom]
for (const c of this.clouds)
for (const p of c.puffs) {
const cx = (c.x + p.dx) * H
const cy = (c.y + p.dy) * H
const rx = p.rx * H
const ry = p.ry * H
const warm = puffy ? 1 : 0.6 + 0.6 * Math.exp(-Math.abs(cx - this.orbX) / (0.5 * H))
for (let y = Math.max(0, Math.floor(cy - ry)); y <= Math.min(H - 1, Math.ceil(cy + ry)); y++)
for (let x = Math.max(0, Math.floor(cx - rx)); x <= Math.min(W - 1, Math.ceil(cx + rx)); x++) {
const dx = (x + 0.5 - cx) / rx
const dy = (y + 0.5 - cy) / ry
const q = dx * dx + dy * dy
if (q >= 1) continue
const under = smoothstep(-1, 1, dy)
const o = (y * W + x) * 3
const a = (puffy ? Math.min(1, (1 - q) * 3) : (1 - q) ** 1.5) * alpha
hdr[o] += (lerp(top[0], bottom[0], under) * warm - hdr[o]) * a
hdr[o + 1] += (lerp(top[1], bottom[1], under) * warm - hdr[o + 1]) * a
hdr[o + 2] += (lerp(top[2], bottom[2], under) * warm - hdr[o + 2]) * a
}
}
}

// A soaring bird seen from the front, wings spread and banking as it circles. A bald eagle has a white head and
// tail and fingered wingtips; a vulture is small and dark with lifted wingtips; an owl has broad rounded wings, a
// round face with ear tufts, and glowing eyes.
Expand Down
Loading
Loading