Proposal: rendering & camera extras — Screen.camera/clip/blend_mode/oval + Camera.follow/shake #23

Closed
opened 2026-08-29 23:39:27 +02:00 by orkun · 1 comment
Owner

Follow-up to #2. The extended Screen.* drawing primitives (line/circle/fill_circle/triangle/fill_triangle/sprite) shipped on branch stdlib-namespaces, but the transform/state-based rendering was deferred because it needs new renderer state in runtime/native/core.ludic.

Proposed surface

  • Screen.camera(pos) — a world-space offset applied in the draw path
  • Screen.clip(rect) — a clip rectangle
  • Screen.blend_mode(m) — additive/mix blending
  • Screen.oval(...) — ellipse outline (midpoint), and Screen.measure_text (alias of the existing text_w)
  • Camera.set(pos), Camera.follow(entity, lerp), Camera.shake(amount) — deterministic shake from the seeded RNG

Work required

A camera offset + clip rect + blend accumulator threaded through the rt_* draw functions.

Blocked on: renderer state in the runtime.

Follow-up to #2. The extended `Screen.*` drawing primitives (`line`/`circle`/`fill_circle`/`triangle`/`fill_triangle`/`sprite`) shipped on branch `stdlib-namespaces`, but the transform/state-based rendering was deferred because it needs **new renderer state** in `runtime/native/core.ludic`. ## Proposed surface - `Screen.camera(pos)` — a world-space offset applied in the draw path - `Screen.clip(rect)` — a clip rectangle - `Screen.blend_mode(m)` — additive/mix blending - `Screen.oval(...)` — ellipse outline (midpoint), and `Screen.measure_text` (alias of the existing `text_w`) - `Camera.set(pos)`, `Camera.follow(entity, lerp)`, `Camera.shake(amount)` — deterministic shake from the seeded RNG ## Work required A camera offset + clip rect + blend accumulator threaded through the `rt_*` draw functions. **Blocked on:** renderer state in the runtime.
orkun added the
proposal
priority:medium
area:rendering
labels 2026-08-29 23:39:27 +02:00
Author
Owner

Shipped in 31cfbc2 (pushed to main) — the transform/state-based rendering this issue tracked as blocked on new renderer state.

The whole proposed surface landed:

  • Screen.camera(pos) / Camera.set — a world-space draw offset; a world point (wx, wy) draws at (wx-x, wy-y).
  • Camera.follow(x, y, lerp) — eases the offset toward centring a target (fixed lerp 0..1).
  • Camera.shake(amount) — +/- amount jitter drawn from the seeded RNG, so a replay shakes identically; shake(0) clears it, and it composes on top of set/follow.
  • Screen.clip(rect) / clip_reset() — a screen-space clip rectangle.
  • Screen.blend_mode(m) — 0 replace (default), 1 additive (clamped at 255) for glow/light.
  • Screen.oval(x, y, rx, ry, color) — axis-aligned ellipse outline (integer midpoint).
  • Screen.measure_text(text) — advance width in the 5x7 font.

On "blocked on renderer state in the runtime": the camera/clip/blend state is threaded through the two framebuffer chokepoints every draw primitive already funnels through (rt_put_px / rt_fill_rect — I confirmed every primitive, including sprites and text, funnels through one of them). That single seam gives the entire draw API the new state at once. Crucially the defaults are neutral (camera (0,0), clip = full screen, blend = replace), so every existing golden render stays byte-identical — all 60+ render tests pass unchanged.

I also added Screen.pixel(x, y) (read a framebuffer pixel), which makes the surface verifiable by reading rendered pixels back rather than trusting a golden hash: examples/library/render.ludic asserts 18 cases — pixel round-trip, camera + Camera.set/follow offsets, clip in/out + reset, additive blend with the 255 clamp, oval extremes vs a hollow centre, and text measurement — all against the actual framebuffer. Wired into x test (now 65 passed). Everything is integer and deterministic (camera, shake, and blend reproduce exactly), so headless renders stay diffable; Camera.follow interpolates in the fixed domain to dodge the int×fixed coercion trap.

Docs: 7 new Screen pages + a new Camera section (3 pages); x check-impl / x check-docs green. Seed reseeded and the C-free bootstrap fixpoint holds. Closing.

Shipped in 31cfbc2 (pushed to `main`) — the transform/state-based rendering this issue tracked as blocked on new renderer state. The whole proposed surface landed: - **`Screen.camera(pos)` / `Camera.set`** — a world-space draw offset; a world point `(wx, wy)` draws at `(wx-x, wy-y)`. - **`Camera.follow(x, y, lerp)`** — eases the offset toward centring a target (fixed `lerp` 0..1). - **`Camera.shake(amount)`** — `+/- amount` jitter drawn from the seeded RNG, so a replay shakes identically; `shake(0)` clears it, and it composes on top of set/follow. - **`Screen.clip(rect)` / `clip_reset()`** — a screen-space clip rectangle. - **`Screen.blend_mode(m)`** — `0` replace (default), `1` additive (clamped at 255) for glow/light. - **`Screen.oval(x, y, rx, ry, color)`** — axis-aligned ellipse outline (integer midpoint). - **`Screen.measure_text(text)`** — advance width in the 5x7 font. **On "blocked on renderer state in the runtime":** the camera/clip/blend state is threaded through the two framebuffer chokepoints every draw primitive already funnels through (`rt_put_px` / `rt_fill_rect` — I confirmed every primitive, including sprites and text, funnels through one of them). That single seam gives the entire draw API the new state at once. Crucially the **defaults are neutral** (camera `(0,0)`, clip = full screen, blend = replace), so every existing golden render stays byte-identical — all 60+ render tests pass unchanged. I also added **`Screen.pixel(x, y)`** (read a framebuffer pixel), which makes the surface verifiable by reading rendered pixels back rather than trusting a golden hash: `examples/library/render.ludic` asserts 18 cases — pixel round-trip, camera + `Camera.set`/`follow` offsets, clip in/out + reset, additive blend with the 255 clamp, oval extremes vs a hollow centre, and text measurement — all against the actual framebuffer. Wired into `x test` (now 65 passed). Everything is integer and deterministic (camera, shake, and blend reproduce exactly), so headless renders stay diffable; `Camera.follow` interpolates in the fixed domain to dodge the int×fixed coercion trap. Docs: 7 new `Screen` pages + a new `Camera` section (3 pages); `x check-impl` / `x check-docs` green. Seed reseeded and the C-free bootstrap fixpoint holds. Closing.
orkun closed this issue 2026-08-31 12:43:20 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#23
No description provided.