diff --git a/.claude/skills/rustmotion/SKILL.md b/.claude/skills/rustmotion/SKILL.md index 35c40dd..bec9ef6 100644 --- a/.claude/skills/rustmotion/SKILL.md +++ b/.claude/skills/rustmotion/SKILL.md @@ -2357,7 +2357,7 @@ cells scattered by a hash of their coordinates. **Checkerboard**: two colours at "size": 9, // cell edge, px "spacing": 22, // lattice pitch, px — clamped to at least `size` "density": 0.75, // 0..1 fraction of cells drawn - "density_ramp": "right", // none | left | right | top | bottom | radial + "density_ramp": "edges", // none | left | right | top | bottom | radial | edges "radius": 1, // cell corner radius; 0 for hard pixels "seed": 7, // stable scatter; same seed → same pattern "motion": "none" // none | twinkle | sweep @@ -2370,7 +2370,7 @@ cells scattered by a hash of their coordinates. **Checkerboard**: two colours at | `size` | `10.0` | Cell edge in px. | | `spacing` | `24.0` | Pitch, **clamped to `size`**: a smaller value would draw a solid sheet and lose the lattice. | | `density` | `0.6` | Fraction of cells drawn. `1.0` fills every cell — required for a real checkerboard. | -| `density_ramp` | `"none"` | Where the field is densest. A ramp is what stops a scatter reading as noise. | +| `density_ramp` | `"none"` | Where the field is densest. A ramp is what stops a scatter reading as noise. `edges` is a vignette — heavy at the border, clear through the middle, so the texture stays off whatever sits in the centre; `radial` is its exact inverse. | | `radius` | `0.0` | `0` keeps the pixels hard-edged; anti-aliasing turns on above `0`. | | `seed` | `7` | Occupancy is a hash of `(col, row, seed)`, so the pattern holds still across frames and is identical between two renders. | | `motion` | `"none"` | `twinkle` fades cells on their own phase; `sweep` runs a band of extra density across the field. Scaled by the background's `speed`. | diff --git a/crates/rustmotion-core/src/schema/background.rs b/crates/rustmotion-core/src/schema/background.rs index 5f4c6eb..e9904d9 100644 --- a/crates/rustmotion-core/src/schema/background.rs +++ b/crates/rustmotion-core/src/schema/background.rs @@ -123,6 +123,14 @@ pub enum PixelDensityRamp { Bottom, /// Dense at the centre, thinning outwards. Radial, + /// Dense at the frame's edges, thinning toward the centre — a vignette. + /// + /// Measured on the reference piece, in a band clear of its window, the + /// density runs 10.9 · 6.5 · 0.8 · 0.2 · 0.2 · 0.2 · 0.2 · 0.9 · 7.8 · 8.8 % + /// across the tenths of the frame: heavy at both edges, effectively empty + /// through the middle 60 %. That is what keeps the texture off whatever + /// sits in the centre, and `Radial` is its exact inverse. + Edges, } /// How a `pixel_grid` animates. diff --git a/crates/rustmotion/src/engine/render/background.rs b/crates/rustmotion/src/engine/render/background.rs index 1b52488..c42ee84 100644 --- a/crates/rustmotion/src/engine/render/background.rs +++ b/crates/rustmotion/src/engine/render/background.rs @@ -431,6 +431,11 @@ fn ramp_at(ramp: PixelDensityRamp, x: f32, y: f32, width: f32, height: f32) -> f let (dx, dy) = (fx - 0.5, fy - 0.5); (1.0 - (dx * dx + dy * dy).sqrt() * 2.0).clamp(0.0, 1.0) } + // The inverse: a vignette that leaves the middle clear. + PixelDensityRamp::Edges => { + let (dx, dy) = (fx - 0.5, fy - 0.5); + ((dx * dx + dy * dy).sqrt() * 2.0).clamp(0.0, 1.0) + } } } @@ -1173,6 +1178,17 @@ mod pixel_grid_tests { // Radial is full at the centre and gone at the corners. assert!(ramp_at(PixelDensityRamp::Radial, 50.0, 50.0, w, h) > 0.99); assert_eq!(ramp_at(PixelDensityRamp::Radial, 0.0, 0.0, w, h), 0.0); + // `Edges` is the vignette: clear in the middle, heavy at the border. + assert_eq!(ramp_at(PixelDensityRamp::Edges, 50.0, 50.0, w, h), 0.0); + assert!(ramp_at(PixelDensityRamp::Edges, 0.0, 50.0, w, h) > 0.99); + assert!(ramp_at(PixelDensityRamp::Edges, 100.0, 50.0, w, h) > 0.99); + // …and the exact inverse of `Radial`, which is the point of having both. + for (x, y) in [(0.0, 0.0), (25.0, 60.0), (100.0, 50.0)] { + let r = ramp_at(PixelDensityRamp::Radial, x, y, w, h); + let e = ramp_at(PixelDensityRamp::Edges, x, y, w, h); + assert!((r + e - 1.0).abs() < 1e-6, "at ({x},{y}): {r} + {e} != 1"); + } + // `None` is uniform: the same everywhere, including the edges. for x in [0.0, 50.0, 100.0] { assert_eq!(ramp_at(PixelDensityRamp::None, x, 0.0, w, h), 1.0);