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
17 changes: 14 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,12 @@ exposure, 1.25 by default.
### layout() for static backgrounds

`layout()` runs after every resize, once the buffers match the new size. Paint whatever never moves there (sky,
mountains, buildings, a tree), copy `hdr` into a field, and start each frame from that copy:
mountains, buildings, a tree); `Canvas` keeps the result in `this.background`, and each frame starts from it:

```ts
protected override layout() {
paintSky(this.hdr, this.W, this.H, this.look.sky, this.look.orb)
paintSky(this.hdr, this.W, this.H, this.look.sky, this.look.orb, this.px)
// …hills, a tree…
this.background = this.hdr.slice()
}

render() {
Expand All @@ -92,6 +91,18 @@ also makes `finish()` cheaper: it blooms and tone-maps whatever `layout()` leave
redoes the 4x4 blocks that differ from it and the bloom they change. So leave `hdr` holding the background at the end
of `layout()`, and avoid full-screen per-frame passes where a static one will do.

The background is supersampled, so every static edge is anti-aliased: `layout()` first runs with `W`, `H` and `hdr`
twice the size, and that painting is box-filtered down into `background`; then it runs again at the real size, for
whatever `render()` reads (positions, ground heights, masks), and that pass's painting is dropped. (Big canvases, such
as snaps, skip the first pass.) Each run must start from scratch, as it already must for resizes. Everything in screen
heights just works. Anything in pixels takes `this.px`, the size of a real pixel in the current pass's pixels: rim
widths (`Math.exp(-depth / (1.5 * this.px))`), offsets (`+ 2 * this.px`), and per-pixel textures, which should stay
on real pixels (`hash2(Math.floor(x / px), Math.floor(y / px))`, `fbm1((x / px) * 0.3)`). `thick()` minimums are
already in real pixels. A per-pixel layer that `render()` draws over the scene (zen's front garden, space's near rings,
city's river) can be made in the first pass, box-filtered with `this.shrink(buffer, channels)`, and kept in a field for
the second pass to use. Scenes whose `layout()` paints nothing set `supersample = 1`. Smooth fills that cost a lot per
pixel (space's nebula) can be worked out once per real pixel.

### Drawing

`Canvas` methods, all in pixels, mixing over what's already there:
Expand Down
Binary file modified screenshots/beach-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/beach.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/city.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/farm-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/farm-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/farm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/prehistoric-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/prehistoric-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/prehistoric.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/space.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/zen-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/zen-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/zen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 65 additions & 9 deletions src/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const BLOOM_THRESHOLD = 0.82
// Bloom changes smaller than this are left out when finish decides which blocks to tone-map again: under 0.06 of a
// level at the steepest point of the tone curve.
const BLOOM_EPSILON = 1e-4
// layout() paints the static background at SUPERSAMPLE times the resolution while that stays within LAYOUT_PIXELS
// (about a 270x70-cell terminal), which keeps a resize under about 150 ms; larger canvases, such as real-pixel mode on
// a big terminal, paint it at their own size.
const SUPERSAMPLE = 2
const LAYOUT_PIXELS = 600_000

export type Part =
| { kind: "capsule"; ax: number; ay: number; bx: number; by: number; r0: number; r1: number; color: RGB; ribs: number }
Expand Down Expand Up @@ -156,8 +161,17 @@ export abstract class Canvas {
return best
}

// Called after every size change, once the buffers match the new size.
// Called after every size change, once the buffers match the new size, to paint what never moves into hdr; Canvas
// keeps the result in `background` for render() to start from. It usually runs twice: first with W, H and hdr
// `supersample` times larger, painting a background that is then box-filtered down so every static edge is
// anti-aliased, then at the real size for everything else render() reads (positions, masks, per-pixel layers), whose
// painting is dropped. `px` is one real pixel in the current pass's pixels: scale pixel-sized widths, offsets and
// textures by it.
protected layout() {}
protected background = new Float32Array(0)
protected px = 1
// 1 for scenes whose layout() paints nothing.
protected supersample = SUPERSAMPLE

resize(W: number, H: number, cells = false) {
W = Math.max(64, Math.round(W))
Expand Down Expand Up @@ -213,16 +227,38 @@ export abstract class Canvas {
this.colB[x] = Math.min(this.bw - 1, i + 1) * 3
this.colF[x] = clamp(bx - i, 0, 1)
}
const S = W * H * this.supersample ** 2 <= LAYOUT_PIXELS ? this.supersample : 1
let fine: Float32Array | undefined
if (S > 1) {
const hdr = this.hdr
this.W = W * S
this.H = H * S
this.px = S
this.hdr = new Float32Array(W * S * H * S * 3)
this.layout()
fine = this.hdr
this.W = W
this.H = H
this.px = 1
this.hdr = hdr
}
this.layout()
keepShadows(this.hdr)
this.still = this.hdr.slice()
this.background = fine ? shrink(fine, W, H, S, 3) : this.hdr.slice()
this.still = this.background
this.stillExposure = NaN
}

// A stroke radius in screen heights, in pixels; on terminal cells never under min, since thinner strokes break up
// into noise there and shimmer as they move.
// Box-filters buf, a buffer of the size being painted in layout()'s supersampled pass with `channels` values per
// pixel, down to the real size; for per-pixel layers that render() reads, kept from that pass for the next.
protected shrink(buf: Float32Array, channels = 3) {
return shrink(buf, this.W / this.px, this.H / this.px, this.px, channels)
}

// A stroke radius in screen heights, in pixels; on terminal cells never under min real pixels, since thinner strokes
// break up into noise there and shimmer as they move.
protected thick(radius: number, min = 1) {
return this.cells ? Math.max(radius * this.H, min) : radius * this.H
return this.cells ? Math.max(radius * this.H, min * this.px) : radius * this.H
}

// Adds light to one pixel.
Expand Down Expand Up @@ -263,7 +299,7 @@ export abstract class Canvas {
// only the outline facing the light catches it; with front lighting each part is shaded as a rounded form.
// light scales the effect (0 draws flat color).
protected shape(parts: Part[], lighting: Lighting, light = 1) {
fillShape(this.hdr, this.W, this.H, parts, lighting, light)
fillShape(this.hdr, this.W, this.H, parts, lighting, light, this.px)
}

// Bloom, vignette and ACES tone mapping from hdr into pixels.
Expand Down Expand Up @@ -315,6 +351,23 @@ function blurLines(src: Float32Array, dst: Float32Array, k0: number, k1: number,
}
}

// Averages each S x S block of src (W * S by H * S pixels of `channels` values) into one pixel.
function shrink(src: Float32Array, W: number, H: number, S: number, channels: number) {
const out = new Float32Array(W * H * channels)
const k = 1 / (S * S)
for (let y = 0; y < H; y++)
for (let sy = 0; sy < S; sy++) {
const row = (y * S + sy) * W * S
for (let x = 0; x < W; x++)
for (let sx = 0; sx < S; sx++) {
const i = (row + x * S + sx) * channels
const o = (y * W + x) * channels
for (let c = 0; c < channels; c++) out[o + c] += src[i + c] * k
}
}
return out
}

function fillDisc(hdr: Float32Array, W: number, H: number, cx: number, cy: number, rad: number, r: number, g: number, b: number, a: number) {
const x0 = Math.max(0, Math.floor(cx - rad - 1))
const x1 = Math.min(W - 1, Math.ceil(cx + rad + 1))
Expand Down Expand Up @@ -354,7 +407,8 @@ function fillEllipse(hdr: Float32Array, W: number, H: number, cx: number, cy: nu
}
}

function fillShape(hdr: Float32Array, W: number, H: number, parts: Part[], lighting: Lighting, light: number) {
// unit is Canvas.px: the rim light reaches 2.2 real pixels in.
function fillShape(hdr: Float32Array, W: number, H: number, parts: Part[], lighting: Lighting, light: number, unit: number) {
if (!parts.length) return
// The shape's overall extent: the pixels it can cover, and the direction its rim light comes from.
let cx0 = Infinity
Expand Down Expand Up @@ -449,7 +503,7 @@ function fillShape(hdr: Float32Array, W: number, H: number, parts: Part[], light
hdr[o + 2] += (c[2] * shade - hdr[o + 2]) * cov
continue
}
const lit = light * clamp((best + 2.2) / 2.2, 0, 1) * Math.max(0, nx * sx + ny * sy) * 0.8
const lit = light * clamp((best + 2.2 * unit) / (2.2 * unit), 0, 1) * Math.max(0, nx * sx + ny * sy) * 0.8
hdr[o] += (c[0] * k + lighting.color[0] * lit - hdr[o]) * cov
hdr[o + 1] += (c[1] * k + lighting.color[1] * lit - hdr[o + 1]) * cov
hdr[o + 2] += (c[2] * k + lighting.color[2] * lit - hdr[o + 2]) * cov
Expand Down Expand Up @@ -621,11 +675,13 @@ function fillPolygon(hdr: Float32Array, W: number, H: number, pts: readonly (rea
const px = x + 0.5
const py = y + 0.5
let inside = false
let dmin = Infinity
let dmin = 0.5
for (let i = 0, j = n - 1; i < n; j = i++) {
const [ax, ay] = pts[j]
const [bx, by] = pts[i]
if (ay > py !== by > py && px < ((bx - ax) * (py - ay)) / (by - ay) + ax) inside = !inside
// Only edges within half a pixel change the coverage.
if (px < Math.min(ax, bx) - 0.5 || px > Math.max(ax, bx) + 0.5 || py < Math.min(ay, by) - 0.5 || py > Math.max(ay, by) + 0.5) continue
const dx = bx - ax
const dy = by - ay
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1e-6)))
Expand Down
6 changes: 3 additions & 3 deletions src/sky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export interface Star {
}

// Fills the whole canvas with a vertical gradient through stops ([height fraction, color]), adds a glow around the orb
// and draws the orb itself.
export function paintSky(hdr: Float32Array, W: number, H: number, stops: [number, RGB][], orb: Orb) {
// and draws the orb itself. px is Canvas.px, for the moon's texture.
export function paintSky(hdr: Float32Array, W: number, H: number, stops: [number, RGB][], orb: Orb, px = 1) {
const ox = orb.x * W
const oy = orb.y * H
const R = orb.r * H
Expand All @@ -52,7 +52,7 @@ export function paintSky(hdr: Float32Array, W: number, H: number, stops: [number
const cov = Math.min(1, Math.max(0, R - r + 0.5))
if (cov <= 0) continue
const limb = 0.85 + 0.15 * Math.sqrt(Math.max(0, 1 - (r / R) ** 2))
const maria = orb.moon ? 1 - 0.18 * smoothstep(0.55, 0.75, fbm1((x - ox) * 0.25 + 3, 7) + fbm1((y - oy) * 0.25 + 1, 9) * 0.6) : 1
const maria = orb.moon ? 1 - 0.18 * smoothstep(0.55, 0.75, fbm1(((x - ox) / px) * 0.25 + 3, 7) + fbm1(((y - oy) / px) * 0.25 + 1, 9) * 0.6) : 1
const m = limb * maria
hdr[o] += (orb.core[0] * m - hdr[o]) * cov
hdr[o + 1] += (orb.core[1] * m - hdr[o + 1]) * cov
Expand Down
9 changes: 5 additions & 4 deletions wallpapers/beach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ class Beach extends Canvas {
private readonly look: Look
private lighting: Lighting = { style: "front", dir: DAYLIGHT }
private readonly shade: Shade
private background = new Float32Array(0)
private stars: Star[]
private clouds: Cloud[]
// Towering cumulus far out on the sea horizon, painted into the background, and shadows of the near clouds.
Expand Down Expand Up @@ -322,7 +321,7 @@ class Beach extends Canvas {
protected override layout() {
const { W, H, A, look } = this
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)
paintSky(this.hdr, W, H, look.sky, look.orb, this.px)
this.weather.cover(this.hdr, W, H)
const sky = this.hdr.slice()
if (!this.weather.covered) paintClouds(this.hdr, W, H, this.towers, look.clouds.top, look.clouds.bottom, look.clouds.alpha, look.orb)
Expand All @@ -336,9 +335,12 @@ class Beach extends Canvas {
}
// Dry sand everywhere below the horizon; the sea covers it down to the waterline each frame.
const [dry] = look.sand
const px = this.px
for (let y = Math.floor(HORIZON * H); y < H; y++)
for (let x = 0; x < W; x++) {
const ripple = 0.92 + 0.08 * Math.sin(x * 0.4 + y * 1.1 + fbm1(x * 0.05 + y * 0.02, 3) * 5) + (hash2(x, y) - 0.5) * 0.06
const u = x / px
const v = y / px
const ripple = 0.92 + 0.08 * Math.sin(u * 0.4 + v * 1.1 + fbm1(u * 0.05 + v * 0.02, 3) * 5) + (hash2(Math.floor(u), Math.floor(v)) - 0.5) * 0.06
const o = (y * W + x) * 3
this.hdr[o] = dry[0] * ripple
this.hdr[o + 1] = dry[1] * ripple
Expand All @@ -359,7 +361,6 @@ class Beach extends Canvas {
{ base: [0.12, 1.03], crown: [0.35, 0.33], lean: 1 },
{ base: [A - 0.1, 1.0], crown: [A - 0.27, 0.39], lean: -1 },
]
this.background = this.hdr.slice()
// Each row of sea mirrors the sky about the horizon, stretched a little as rough water does, its ripples wobbling
// wider toward the shore.
const y0 = Math.floor(HORIZON * H)
Expand Down
46 changes: 26 additions & 20 deletions wallpapers/city.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,10 @@ class City extends Canvas {
private readonly look: Look
private lighting: Lighting = { style: "front", dir: DAYLIGHT }
private readonly shade: Shade
private background = new Float32Array(0)
// The river with the skyline reflected in it, which each frame ripples sideways where `water` is set.
private river = new Float32Array(0)
// The supersampled layout pass's river, box-filtered, for the real-size pass.
private fineRiver: Float32Array<ArrayBuffer> | undefined
private water = new Uint8Array(0)
private stars: Star[]
private visibleStars: Star[] = []
Expand Down Expand Up @@ -357,7 +358,7 @@ class City extends Canvas {
protected override layout() {
const { W, H, A, look } = this
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)
paintSky(this.hdr, W, H, look.sky, look.orb, this.px)
this.weather.cover(this.hdr, W, H)
const sky = this.hdr.slice()
this.beacons = []
Expand Down Expand Up @@ -406,8 +407,12 @@ class City extends Canvas {
this.water = new Uint8Array(W * H)
for (let i = Math.floor(BANK * H) * W; i < Math.min(H, Math.ceil(QUAY * H)) * W; i++) this.water[i] = this.hdr[i * 3] === this.river[i * 3] && this.hdr[i * 3 + 2] === this.river[i * 3 + 2] ? 1 : 0
this.paintLamps()
if (this.px > 1) this.fineRiver = this.shrink(this.river)
else if (this.fineRiver) {
this.river = this.fineRiver
this.fineRiver = undefined
}
if (this.wet) this.paintWetStreet()
this.background = this.hdr.slice()
if (this.warm) return
this.warm = true
for (let i = 0; i < 400; i++) this.stepTraffic(0.25)
Expand Down Expand Up @@ -457,7 +462,7 @@ class City extends Canvas {
// A tower drawn pixel by pixel: a facade with a shaded side by day, rim-lit edges at sunset and night, and a grid,
// bands or strips of windows, some of them lit.
private paintTower(t: Tower) {
const { W, H, A, look } = this
const { W, H, A, look, px } = this
const day = look.style === "front"
const top = t.tiers[t.tiers.length - 1]
const cx = (t.tiers[0].x0 + t.tiers[0].x1) / 2
Expand Down Expand Up @@ -488,11 +493,12 @@ class City extends Canvas {
let g = body[1] * shade
// The shaded side takes the blue of the sky.
let b = body[2] * shade * (side < 1 ? 1.25 : 1)
const wu = u - 0.004
const wv = v - 0.006
// Windows are finer than the supersampled layout's pixels can show, so they keep to real pixels.
const wu = ((Math.floor(x / px) + 0.5) * px) / H - tier.x0 - 0.004
const wv = ((Math.floor(y / px) + 0.5) * px) / H - tier.top - 0.006
const fu = wu / cw - Math.floor(wu / cw)
const fv = wv / ch - Math.floor(wv / ch)
const inside = wu > 0 && wv > 0 && u < width - 0.003 && (t.windows === 0 ? fu < 0.55 && fv < 0.6 : t.windows === 1 ? fv < 0.5 : fu < 0.45)
const inside = wu > 0 && wv > 0 && wu + 0.004 < width - 0.003 && (t.windows === 0 ? fu < 0.55 && fv < 0.6 : t.windows === 1 ? fv < 0.5 : fu < 0.45)
if (inside && (!t.far || !day)) {
const col = Math.floor(wu / (t.windows === 1 ? cw * 2.5 : cw))
const row = Math.floor(wv / (t.windows === 2 ? ch * 1.5 : ch))
Expand All @@ -517,7 +523,7 @@ class City extends Canvas {
}
if (rimGlow > 0) {
const edge = (sunLeft ? u : width - u) * H
const rim = (Math.exp(-edge / 1.5) + 0.6 * Math.exp(-v * H)) * rimGlow
const rim = (Math.exp(-edge / (1.5 * px)) + 0.6 * Math.exp((-v * H) / px)) * rimGlow
r += look.light[0] * rim
g += look.light[1] * rim
b += look.light[2] * rim
Expand All @@ -538,7 +544,7 @@ class City extends Canvas {
this.beacons.push([mid / H, top.top - 0.07])
}
if (t.crown === 3) {
this.polygon([[mid - w / 2, roof + 1], [mid, roof - 0.06 * H], [mid + w / 2, roof + 1]], dark)
this.polygon([[mid - w / 2, roof + this.px], [mid, roof - 0.06 * H], [mid + w / 2, roof + this.px]], dark)
this.shape([cap(mid, roof - 0.05 * H, mid, roof - 0.1 * H, 0.002 * H, 0.001 * H, dark)], this.lighting, 0.5)
this.beacons.push([mid / H, top.top - 0.1])
}
Expand Down Expand Up @@ -581,7 +587,7 @@ class City extends Canvas {
const on = lit && this.look.glow > 0
const k = on ? this.look.glow : 0.3
const tube: RGB = [s.color[0] * k, s.color[1] * k, s.color[2] * k]
const r = Math.max(0.55, 0.0013 * H)
const r = Math.max(0.55 * this.px, 0.0013 * H)
const inset = 0.0025
const [x0, y0, x1, y1] = [(s.x0 + inset) * H, (s.y0 + inset) * H, (s.x1 - inset) * H, (s.y1 - inset) * H]
const parts: Part[] = [cap(x0, y0, x1, y0, r, r, tube), cap(x1, y0, x1, y1, r, r, tube), cap(x1, y1, x0, y1, r, r, tube), cap(x0, y1, x0, y0, r, r, tube)]
Expand All @@ -600,23 +606,23 @@ class City extends Canvas {
}
this.shape(parts, this.lighting, 0)
if (!on) return
this.glow(this.hdr, ((s.x0 + s.x1) / 2) * H, ((s.y0 + s.y1) / 2) * H, (s.x1 - s.x0) * H * 1.2 + 4, (s.y1 - s.y0) * H * 0.9 + 4, s.color, 0.12 * k)
this.glow(this.hdr, ((s.x0 + s.x1) / 2) * H, ((s.y0 + s.y1) / 2) * H, (s.x1 - s.x0) * H * 1.2 + 4 * this.px, (s.y1 - s.y0) * H * 0.9 + 4 * this.px, s.color, 0.12 * k)
}

// The river darkens toward the near quay and mirrors the skyline, the reflection stretched downward into streaks and
// broken by ripples.
private paintRiver() {
const { W, H, hdr, look } = this
const { W, H, hdr, look, px } = this
const r0 = Math.floor(BANK * H)
const r1 = Math.min(H, Math.ceil(QUAY * H))
const [far, near] = look.river
const smear = Math.max(2, Math.round(0.006 * H))
const smear = Math.max(2 * px, Math.round(0.006 * H))
for (let y = r0; y < r1; y++) {
const depth = (y - r0) / (r1 - r0)
const sy = r0 - 1 - Math.floor((y - r0) * 0.7)
const sy = r0 - px - Math.floor((y - r0) * 0.7)
const k = lerp(1, 0.6, depth) * look.reflect
for (let x = 0; x < W; x++) {
const ripple = 0.88 + 0.12 * Math.sin(x * 0.12 + y * 0.9 + fbm1(x * 0.04 + y * 0.17, 3) * 5)
const ripple = 0.88 + 0.12 * Math.sin((x / px) * 0.12 + (y / px) * 0.9 + fbm1((x / px) * 0.04 + (y / px) * 0.17, 3) * 5)
let r = 0
let g = 0
let b = 0
Expand Down Expand Up @@ -720,18 +726,18 @@ class City extends Canvas {

// After rain the street mirrors the underside of the highway, its pillars, lamps and the sign hung on one.
private paintWetStreet() {
const { W, H, hdr } = this
const { W, H, hdr, px } = this
const s0 = Math.floor(STREET * H)
for (let y = s0; y < H; y++) {
const sy = s0 - 1 - Math.floor((y - s0) * 0.8)
const sy = s0 - px - Math.floor((y - s0) * 0.8)
for (let x = 0; x < W; x++) {
const puddle = 0.35 + 0.65 * smoothstep(0.4, 0.62, fbm1(x * 0.03 + Math.floor(y * 0.4) * 5.3, 2))
const streak = 0.7 + 0.3 * Math.sin(x * 1.3 + y * 0.2)
const puddle = 0.35 + 0.65 * smoothstep(0.4, 0.62, fbm1((x / px) * 0.03 + Math.floor((y / px) * 0.4) * 5.3, 2))
const streak = 0.7 + 0.3 * Math.sin((x / px) * 1.3 + (y / px) * 0.2)
let r = 0
let g = 0
let b = 0
for (let j = 0; j < 3; j++) {
const o = (clamp(sy - j * 2, 0, H - 1) * W + x) * 3
const o = (clamp(sy - j * 2 * px, 0, H - 1) * W + x) * 3
r += hdr[o]
g += hdr[o + 1]
b += hdr[o + 2]
Expand Down
Loading
Loading