← stage · version 0.18.10
136 public items, in source order. Generated from the registry index, which re-derives them from the package source at publish time — so a signature here is the one that version actually ships.
pub struct StageNode { nd_parent: integer, nd_x: float, nd_y: float, nd_rot: float, nd_sx: float, nd_sy: float, nd_ox: float, nd_oy: float, nd_w: float, nd_h: float, nd_colour: integer, nd_visible: boolean, nd_atlas: integer, nd_u0: float, nd_v0: float, nd_u1: float, nd_v1: float, nd_alpha: float, nd_mask: integer, nd_clips: boolean, nd_clipped: boolean, nd_cx0: float, nd_cy0: float, nd_cx1: float, nd_cy1: float, nd_key: text, nd_action: text, nd_facing: float, nd_mirror: boolean, nd_seq: integer, nd_elapsed: integer, nd_cols: integer, nd_rows: integer, nd_sway: float, nd_phase: float, nd_cue: float, nd_layer: integer, nd_depth: float, nd_disabled: boolean, nd_focusable: boolean, nd_a: float = 1.0, nd_b: float = 0.0, nd_c: float = 0.0, nd_d: float = 1.0, nd_tx: float = 0.0, nd_ty: float = 0.0 }
A node's local placement, plus the world affine compose derives for it.
⚠ NAMED StageNode AND NOT Node, because a type name is GLOBAL across the dependency graph and mesh3d — which graphics pulls in — already publishes a Node for its 3-D scene. The compiler refuses the collision outright rather than resolving it silently, which is the good outcome; the cost of finding it after publishing would have been a rename nobody can make.
The affine is 2x3 and is applied as x' = a*lx + c*ly + tx y' = b*lx + d*ly + ty which is the layout the instanced draw path consumes unchanged.
pub struct Layer { ly_parallax: float, ly_fog: integer, ly_density: float, ly_blur: integer }
Everything one ordering band carries: how much of the camera it feels, what atmosphere stands in front of it, and how far out of focus it is.
⚠ ONE RECORD, NOT THREE PARALLEL VECTORS. Parallax lived in a bare vector<float> and fog and blur were about to join it as siblings — which is exactly the shape that put a sequence's name on another sequence's cells in P5: every list stays well-formed on its own and only the correspondence goes wrong, silently. A layer's facts travel together.
pub struct View { vw_cam_x: float, vw_cam_y: float, vw_x: integer, vw_y: integer, vw_w: integer, vw_h: integer }
A way of LOOKING at the stage: a camera and the screen rect it paints into.
⚠ A VIEW IS PRESENTATION, NEVER WORLD. The scene cannot tell how many are looking at it — which is the split this phase exists to enforce: the world is deterministic and shared, the presentation is local and free. Window size, camera and ambient motion may differ per viewer and must be allowed to.
pub struct StageLight { lt_x: float, lt_y: float, lt_r: float, lt_colour: integer, lt_power: float }
A point light: where it stands, how far it reaches, and what colour it casts.
⚠ IT LIVES IN WORLD SPACE, like everything else the app places. A light is not attached to a node, so moving a torch-bearer means moving the light too — deliberate, because a light parented to a sprite would inherit that sprite's rotation and scale, and neither means anything to a point light. ⚠ StageLight, not Light: a TYPE name is global across every package in the build, and mesh3d — pulled in by graphics — already publishes a Light. The compiler refusing is the good outcome; after publishing, this rename is one nobody can make. Same reason the node is StageNode.
pub struct Stage { st_nodes: vector<StageNode>, st_masks: vector<Mask>, st_capture: integer, st_order: vector<integer>, st_cue: boolean, st_cue_near: float, st_cue_far: float, st_cue_scale: float, st_cue_haze: float, st_haze_colour: integer, st_lights: vector<StageLight>, st_ambient: float, st_light_map: boolean, st_hud_layer: integer, st_views: vector<View>, st_layers: vector<Layer>, st_uploads: integer, st_dirty: boolean, st_time: float, st_seqs: vector<Sequence>, st_proj: integer, st_tweens: vector<NodeTween>, st_ptr_x: float, st_ptr_y: float, st_pointer: integer, st_hover: integer, st_focus: integer, st_fields: vector<NodeField>, st_clip: text }
A 2D scene and everything that drives it: its nodes and masks, the draw order compose derives, the views, lights and depth cue it paints with, and its pointer, focus and tween state. stage_new makes an empty one; every other call changes it.
pub struct Sequence { q_first: integer, q_count: integer, q_fps: float, q_mode: integer, q_name: text }
A run of atlas cells played at a rate.
⚠ THE NAME LIVES HERE, not in a list beside st_seqs. A parallel vector that only the naming registrar appended to drifted the moment a caller mixed add_sequence with add_named_sequence, and every later name then pointed at somebody else's cells — silently, because both lists were individually well-formed. One home per fact; the test that caught it is kept.
pub struct Mask { m_w: integer, m_h: integer, m_alpha: vector<integer> }
An alpha mask: per-texel coverage, row-major, 0..255.
A sprite is mostly holes, so a rect test answers the wrong question — you can SEE through a tree, therefore you must be able to CLICK through it. A node without a mask is solid, which is the right default for a panel or a tile.
pub struct Place { pl_w: float = 0.0, pl_h: float = 0.0, pl_colour: integer = 0, pl_visible: boolean = true, pl_atlas: integer = 0, pl_u0: float = 0.0, pl_v0: float = 0.0, pl_u1: float = 1.0, pl_v1: float = 1.0, pl_alpha: float = 1.0, pl_mask: integer = -1, pl_clips: boolean = false, pl_key: text = "", pl_seq: integer = -1, pl_cols: integer = 1, pl_rows: integer = 1, pl_sway: float = 0.0, pl_layer: integer = 0, pl_depth: float = 0.0, pl_x: float = 0.0, pl_y: float = 0.0, pl_rot: float = 0.0, pl_sx: float = 1.0, pl_sy: float = 1.0, pl_ox: float = 0.0, pl_oy: float = 0.0 }
Where to put a node, and how. A struct rather than nine parameters because the compiler said so — too-many-parameters fires at eight, and a caller's burden is fixed by a struct where a reader's is fixed by an extracted function.
⚠ THE DEFAULTS ARE THE POINT, not convenience. Scale defaults to 1.0 and not to the zero an omitted field would otherwise take: a scale of 0 is a *meaningful* value of the field's domain — it collapses the node to nothing — so an author who writes Place { pl_x: 10.0, pl_y: 20.0 } and gets an invisible node has been handed a legal answer to a question they did not ask. That is exactly the case omitted-field-zero exists to warn about, answered here at the declaration.
pub struct Bounds { b_x0: float, b_y0: float, b_x1: float, b_y1: float }
A point, so a caller can ask where something ended up without unpacking six floats. world_point is the only honest way to check a transform: a matrix means nothing except through what it does to a point.
pub struct P2 { p_x: float, p_y: float }
A point in world coordinates, as world_point and world_origin answer it.
pub fn stage_new() -> Stage
An empty stage: no nodes, one view, the camera at the origin and ambient light full. Everything else here is a mutation of this.
pub fn node_count(self: const Stage) -> integer
How many nodes the tree holds. Every index this library hands out is below it, so it is also the bound for a walk over the scene.
pub fn node_add(self: Stage, parent: integer, p: const Place) -> integer
Add a node and answer its index. parent is -1 for a root.
⚠ Refuses a forward parent reference by answering -1. compose is a single forward pass, so a child placed before its parent would read an unwritten world transform — silently, and the frame would look almost right.
pub fn compose(self: Stage)
Derive every node's world affine in one forward pass.
A node's local transform is T(x, y) . R(rot) . S(sx, sy) . T(-ox, -oy), which is what makes the ORIGIN the anchor: the local point (ox, oy) maps exactly to (x, y), so a sprite whose origin sits at its feet stands up from where it is placed. World = parent's world composed with that.
pub fn depth_cue(self: Stage, near: float, far: float, far_scale: float, far_haze: float, haze_colour: integer)
Turn the depth cue on. near and far are depths; a node at far is drawn at far_scale of its size and hazed far_haze toward haze_colour, and one at near is untouched. Off by default, and off means an identical picture.
This is the 2.5-D FEEL: distance reads as smaller and less distinct, which is what separates a mob standing behind a wall from one standing in front of it even when neither overlaps the other. Real blur belongs to the per-layer pass; a haze toward the fog colour costs one lerp and carries most of the effect.
pub fn cue_of(self: const Stage, depth: float) -> float
How far through the cue a depth sits: 0 at the near plane, 1 at the far one, clamped outside them so a stray depth cannot invert the effect.
pub fn haze_of(self: const Stage, idx: integer) -> float
How hazed node idx is, 0..1 — what a renderer fades toward the haze colour.
pub fn order(self: const Stage) -> vector<integer>
The draw order, derived by compose: indices, back to front.
pub fn bounds_of(self: const Stage, idx: integer) -> Bounds
A node's axis-aligned bounds in world space.
⚠ AXIS-ALIGNED, so a ROTATED clipper clips to its bounding box rather than to its turned rectangle. A scissor is axis-aligned in hardware and there is no honest way to make it otherwise; a rotated clip needs a stencil, which this arc does not have. Stated rather than discovered.
pub fn world_point(self: const Stage, idx: integer, lx: float, ly: float) -> P2
Where a point in node idx's own space lands in world space. Valid after compose; answers (0, 0) for an index that is not a node.
pub fn world_origin(self: const Stage, idx: integer) -> P2
Where the node's ORIGIN is — the point A7's depth key sorts on.
pub struct UiRect { r_x: integer, r_y: integer, r_w: integer, r_h: integer }
── A2: the draw list ────────────────────────────────────────────────────────
⚠ THESE THREE TYPES ARE NOT NEW. Their field names and types are copied EXACTLY from lavition_ui, which produces vector<DrawRect> from panel_draw_list today and is proven by a 12 k-line editor. Matching a working command shape rather than minting a rival one is the whole point: a game's UI and an editor's UI have to reach the GPU by one path, or the batcher sees two.
⚠ AND THEY NEED ONE HOME, WHICH THEY DO NOT YET HAVE. lavition_ui is unpublished, so its DrawRect and this one are two nominal types with one shape. Until it is published (@PLN145 D0) the types cannot literally be shared, and one of the two must eventually give — see the plan.
pub struct DrawRect { dr_rect: UiRect, dr_colour: integer }
A packed 0xRRGGBB colour, as lavition_ui carries it.
pub struct DrawText { dt_text: text, dt_x: integer, dt_y: integer, dt_colour: integer }
One line of text to draw, in the command shape lavition_ui takes: the text, its left edge and its TOP, and a packed 0xRRGGBB colour.
pub fn draw_list(self: const Stage) -> vector<DrawRect>
Emit the scene as a flat command list, in index order.
Index order IS the draw order for now: A4 gives depth its own key, and doing it here would be a second ordering rule to keep in step with that one. A node with no area emits nothing — a zero size is how a pure transform parent says it is scaffolding rather than a thing to look at.
pub fn render(list: const vector<DrawRect>, cv: graphics::Canvas)
Paint a command list onto a software canvas.
⚠ THE ALPHA BYTE IS ADDED HERE, and it has to be. A DrawRect carries 0xRRGGBB — lavition_ui says so, and keeps it a plain integer so it needs no graphics dependency — while graphics::Canvas reads 0xAARRGGBB and treats a missing top byte as FULLY TRANSPARENT. That is graphics' own trap: the colour looks right in the source and paints nothing. One | 0xff000000 at the boundary is the whole fix, and it belongs at the boundary rather than in every caller.
pub fn opaque(colour: integer) -> integer
0xRRGGBB -> 0xAARRGGBB, leaving an already-opaque colour alone.
pub struct Batch { b_layer: integer, b_atlas: integer, b_first: integer, b_count: integer, b_clipped: boolean, b_cx0: float, b_cy0: float, b_cx1: float, b_cy1: float }
One run of consecutive instances that share an atlas.
pub fn batches(self: const Stage) -> vector<Batch>
Split the visible nodes into runs, in scene order.
pub fn pack_instances(self: const Stage) -> vector<single>
Pack every visible node into one instance buffer, in the same order batches counted. The two walk the scene by the same rule, so a batch's b_first and b_count index this buffer directly.
pub struct Painter { pn_shader: integer, pn_vao: integer, pn_ivbo: integer, pn_cap: integer, pn_light: integer, pn_fbo: integer, pn_scene: integer, pn_fw: integer, pn_fh: integer }
The GL objects a stage needs, built once.
pub fn painter_new() -> Painter
Compile the shader and upload the unit quad. Call once, after a window exists.
pub fn gl_render(self: Stage, p: Painter, screen_w: float, screen_h: float) -> integer
Draw the scene, one instanced call per run. Answers the number of draw calls issued, which is the half of the gate that pixels cannot see: a batch that fell back to one draw per sprite would look identical and cost N.
pub fn scissor_of(self: const Stage, view: integer, b: const Batch, coff: const P2) -> Bounds
The screen rect a run may paint in: the view's own rect, intersected with the batch's clip once that clip is moved by the same offset as its content.
An unbounded rect is spelled as the whole target rather than as a flag, so the intersection is arithmetic in every case and the unsplit stage is the degenerate cell rather than a second path.
pub fn add_mask(self: Stage, w: integer, h: integer, alpha: const vector<integer>) -> integer
Register an alpha mask and answer its index, for Place.pl_mask.
pub fn to_local(self: const Stage, idx: integer, sx: float, sy: float) -> P2
Take a screen point into node idx's own space.
The world affine is 2x2 plus a translation, so the inverse is closed form — det = a*d - c*b, and no general 4x4 inversion is involved. A degenerate node (zero scale) has no inverse and answers its own origin rather than an infinity.
pub fn hits(self: const Stage, idx: integer, sx: float, sy: float) -> boolean
Is this screen point ON node idx — inside its rect AND, where it has a mask, on a texel that is not a hole?
pub fn pick(self: const Stage, sx: float, sy: float) -> integer
The topmost node under this screen point, or -1.
Walks the draw order BACKWARDS, so the node drawn last — the one on top — is tested first, and insertion order breaks a tie the same way it does when drawing. One order, two consumers: a pick that disagreed with the picture would be worse than no pick at all.
pub fn release(self: Stage, sx: float, sy: float) -> integer
A release answers the node that took the PRESS, wherever the pointer is now.
⚠ THE CAPTURE IS THE POINT, and it is the half everyone forgets. Without it a button fires when you press it and slide away, and never fires when you press it and drift a pixel — both of which feel broken and neither of which a rect test can explain. A release with nothing captured answers -1.
pub fn released_on(self: const Stage, idx: integer, sx: float, sy: float) -> boolean
Did the release land on the node that was pressed? A button fires only when this is true; release still reports the captured node either way.
pub fn capture(self: const Stage) -> integer
The node press is holding, or -1. A drag reads this to know what it is dragging — the capture survives the pointer leaving the node, which is what makes dragging off a button and back onto it work.
pub enum UiState { Normal, Over, Down, Disabled }
The four states a widget node can be in.
⚠ Disabled outranks the other three: a node that cannot be used must not look pressable even while the pointer is on it.
pub fn set_pointer(self: Stage, kind: integer)
Say whether the pointer is a mouse or a finger. Mouse is the default.
pub fn pointer(self: const Stage) -> integer
Which kind is in force. ui_state reads it to decide whether Over is a state this device can show at all — a finger is never merely hovering.
pub fn pointer_move(self: Stage, sx: float, sy: float)
The pointer moved without a button. A touch stream never sends this.
pub fn hovered(self: const Stage) -> integer
The node the pointer is over, or -1. Answers even under touch, where it is the node the finger is ON — ui_state is what declines to call that Over.
pub fn enabled(self: const Stage, idx: integer) -> boolean
Is this node in use? False for an index out of range too, so a stale index reads as unusable rather than as a live widget.
pub fn ui_state(self: const Stage, idx: integer) -> UiState
What this node looks like right now.
pub fn click(self: Stage, sx: float, sy: float) -> integer
The release door for a widget: answers the node that FIRES, or -1.
⚠ **Use this OR release, not both** — either ends the press. release answers *who took the press* and leaves the fired/not decision to the caller; this answers the question a button actually asks. It exists because combining release with released_on by hand is where press-then-leave-then-release goes wrong, and that is a mistake a library can simply not offer.
pub fn set_focusable(self: Stage, idx: integer, on: boolean)
Put a node into the tab order, or take it out. focus_next / focus_prev walk exactly the nodes this says yes to, in declaration order.
pub fn focused(self: const Stage) -> integer
Which node has the keyboard, or -1.
pub fn set_focus(self: Stage, idx: integer)
Give it the keyboard. A node that cannot take it clears the focus instead, so this never leaves the keyboard somewhere unreachable.
pub fn focus_next(self: Stage)
⚠ TAB ORDER IS THE DECLARED ORDER — the order the nodes were added, which is the order a reader of the building code sees. A separate tab_index would be a second ordering to keep in step with the first, and the two disagree the moment someone inserts a node.
pub fn focus_prev(self: Stage)
One step back through that same order — the same walk reversed, so the two directions cannot disagree about what is focusable.
pub fn set_field(self: Stage, idx: integer, s: text)
Make this node an editable field holding s, and give it the tab order.
pub fn is_field(self: const Stage, idx: integer) -> boolean
Does this node carry an editable text field? Every field_* reader answers a harmless default where this is false, so asking first is optional.
pub fn field_text(self: const Stage, idx: integer) -> text
What the field holds right now. The buffer is the truth; the caret and the mark are positions into it, counted in CHARACTERS.
pub fn field_caret(self: const Stage, idx: integer) -> integer
Where the caret is, counted in CHARACTERS.
pub fn field_mark(self: const Stage, idx: integer) -> integer
The selection anchor. Equal to the caret when nothing is selected.
pub fn field_selection(self: const Stage, idx: integer) -> text
The selected text, or "" when the caret is a point.
pub fn field_place_caret(self: Stage, idx: integer, at_char: integer, extend: boolean)
Put the caret at a CHARACTER index — what a click resolves to once whoever draws the text has turned an x-coordinate into a count.
pub fn clipboard(self: const Stage) -> text
Our own clipboard — see the header.
pub fn set_clipboard(self: Stage, s: text)
Fill it — how the host's clipboard reaches a paste.
pub fn text_event(self: Stage, s: text)
Commit text into the focused field, replacing any selection.
⚠ This is where IME text arrives: EV_TEXT delivers a whole COMMITTED string, which may be several characters and is very often not ASCII.
pub fn key_event(self: Stage, key: integer, mods: integer)
A key, with the modifiers that arrived WITH it. Tab moves the focus; every other key goes to the focused field.
pub fn render_stage(self: const Stage, cv: graphics::Canvas)
Paint the scene onto a software canvas, honouring each node's alpha and tint.
render(list, canvas) stays the path for a lavition_ui command list, which carries 0xRRGGBB and no alpha by design. This is the stage's own path, where a node has an alpha to respect — and the two must agree wherever alpha is 1.
pub fn render_view(self: const Stage, view: integer, cv: graphics::Canvas)
Paint one VIEW of the scene: the same nodes, through that view's camera, and confined to its own screen rect.
⚠ THE VIEWPORT NARROWS THE SPAN, it does not test each pixel — the same technique A6's clip uses, so the boundary falls where the arithmetic puts it rather than where a per-pixel predicate rounds it, and the two cannot disagree about a shared edge.
pub fn set_camera(self: Stage, x: float, y: float)
Move the camera. Does NOT dirty the instance buffer — that is the point.
An unsplit stage IS a one-view stage, so this is view 0's camera rather than a second place a camera can live — the same move P2 made with parallax, where flat scrolling is every factor at 1.0 instead of a separate path.
pub fn camera_x(self: const Stage) -> float
Where the camera stands. An unsplit stage IS a one-view stage, so this is view 0 rather than a second place a camera can live.
pub fn camera_y(self: const Stage) -> float
The other half of the same pair.
pub fn set_parallax(self: Stage, layer: integer, factor: float)
How much of the camera a layer feels. 1.0 is the world; below that is a background that lags; above it a foreground that leads. Flat scrolling is every factor at 1.0, which is the degenerate case rather than a second code path.
pub fn parallax_of(self: const Stage, layer: integer) -> float
What a layer's factor is now. A layer nobody configured reads 1.0 — the world's own rate — so asking about one is always safe.
pub fn cam_offset(self: const Stage, layer: integer) -> P2
The screen-space offset a layer's content is drawn at, in view 0.
pub fn uploads(self: const Stage) -> integer
How many times the instance buffer has been written. A pure camera move must not move this — the property the whole design turns on.
pub fn set_time(self: Stage, t: float)
pub fn time_of(self: const Stage) -> float
The ambient clock, which is the t every sway phase is derived from.
pub fn sway_of(self: const Stage, idx: integer) -> float
How far node idx is displaced right now, in pixels. Bounded by its amplitude by construction, since it is an amplitude times a sine.
pub fn add_sequence(self: Stage, first: integer, count: integer, fps: float, mode: integer) -> integer
Register a sequence and answer its index, for Place.pl_seq.
pub fn advance(self: Stage, dt_us: integer)
Step every animated node by dt_us MICROSECONDS — the simulation's own delta, not a clock this library reads.
⚠ Dirties the instance buffer, unlike the camera and the ambient clock — an animated sprite genuinely changes one number per frame. That number is a FRAME INDEX and not a uv rect, so it is one float and not four.
pub fn restart(self: Stage, idx: integer)
Reset a node to the start of its sequence.
pub enum Prop { X, Y, Rot, ScaleX, ScaleY, OriginX, OriginY, Alpha, Depth }
Which of a node's numbers a tween drives.
⚠ Every one is a float the node already stores. Colour, atlas and the sequence are not here on purpose: they are not numbers you can be *half way* between, and interpolating them would produce values the node never holds.
pub fn prop_of(self: const Stage, idx: integer, p: Prop) -> float
What that property reads right now.
pub fn set_prop(self: Stage, idx: integer, p: Prop, v: float)
Write it now. The immediate door — the tween door writes through the same switch, one frame at a time.
pub fn tween_prop(self: Stage, idx: integer, p: Prop, to: float, dur_us: integer, c: Curve = Curve.Linear, s: Shape = Shape.In)
Move p from where it is now to to, over dur_us microseconds.
⚠ from is read at THIS moment, so a tween started mid-flight carries on from where the last one had got to rather than snapping back to a remembered start. Replaces any tween already on this (node, property).
pub fn prop_tweening(self: const Stage, idx: integer, p: Prop) -> boolean
Is that property still moving?
pub fn cancel_prop_tween(self: Stage, idx: integer, p: Prop)
Stop it wherever it has got to. The property keeps its current value.
pub fn cancel_node_tweens(self: Stage, idx: integer)
Drop every tween on a node.
pub fn tween_count(self: const Stage) -> integer
How many property tweens the stage is holding — the gate reads this.
pub fn set_projection(self: Stage, mode: integer)
Pick TOP_DOWN or SIDE_ON. It decides what face does with an angle: rotate the one sprite, or resolve a sequence and mirror it.
pub fn projection(self: const Stage) -> integer
Which of the two models is in force.
pub fn sequence_count(self: const Stage) -> integer
How many sequences are registered; also the bound on a pl_seq index.
pub fn add_named_sequence(self: Stage, name: text, first: integer, count: integer, fps: float, mode: integer) -> integer
Register a sequence under a NAME, so face and set_action can resolve it. The convention is <key>, <key>_<action>, <key>_<action>_<facing>, and resolution walks it most-specific first.
pub fn facing_name(angle: float) -> text
Which of the four compass facings an angle points at.
⚠ SCREEN SPACE, Y DOWN: angle 0 is +x (east) and +pi/2 is +y (south), matching pl_rot and the shader. The edges are HALF-OPEN, as A4's picking rects are — 45 degrees belongs to south — so no angle is claimed by two facings.
pub fn face(self: Stage, idx: integer, angle: float)
Point a node at angle radians. Top-down turns it; side-on picks and flips.
pub fn set_action(self: Stage, idx: integer, action: text)
What a node is doing — "idle", "walk", "attack". Re-resolves, phase intact.
pub fn facing_of(self: const Stage, idx: integer) -> float
Where the node points, in radians. Kept in BOTH models — side-on holds the angle even though what it draws for it is a chosen sequence and a flip.
pub fn action_of(self: const Stage, idx: integer) -> text
What the node is doing, as set_action last left it. Name resolution reads this together with the key and the facing.
pub fn rotation_of(self: const Stage, idx: integer) -> float
The sprite's own rotation, which side-on leaves alone.
pub fn mirrored(self: const Stage, idx: integer) -> boolean
Is this node drawn flipped? Only side-on ever sets it: mirroring is how the other half of the facings exists without another atlas cell.
pub fn view_count(self: const Stage) -> integer
How many views the stage paints into: view 0, plus one per add_view.
pub fn add_view(self: Stage, x: integer, y: integer, w: integer, h: integer) -> integer
Add a view painting into the screen rect (x, y, w, h). A width of 0 or less means unbounded — the whole target, which is what view 0 starts as.
pub fn set_view_rect(self: Stage, view: integer, x: integer, y: integer, w: integer, h: integer)
Move or resize a view's rectangle on screen. The camera it carries is left alone, so a split-screen resize does not move what either half is looking at.
pub fn set_view_camera(self: Stage, view: integer, x: float, y: float)
Move one view's camera. Like set_camera, it does not dirty the buffer.
pub fn view_camera_x(self: const Stage, view: integer) -> float
Where this view's camera stands. A view out of range answers 0.0 rather than failing, which is view_at's verdict and not a second rule.
pub fn view_offset(self: const Stage, view: integer, layer: integer) -> P2
Where a layer's content lands on screen in this view: the camera un-applied by the layer's parallax, then moved into the view's rect.
⚠ THE VIEWPORT ORIGIN IS PART OF THE MAPPING, not decoration. Leave it out and both views draw on top of each other while every camera test still passes.
pub fn sync_instances(self: Stage) -> boolean
Pack the instance buffer if the scene changed, and say whether it did.
One home for *when does the buffer get rewritten*, so the GL path and any count of uploads cannot disagree about it — and so N views can be shown to cost ONE upload without a GL context to prove it in.
pub fn pick_in(self: const Stage, view: integer, sx: float, sy: float) -> integer
The topmost node under a screen point AS THAT VIEW SEES IT, or -1.
⚠ ONE SCREEN POINT IS A DIFFERENT WORLD POINT IN EACH VIEW — the same reason P2 un-applies the camera per LAYER. A point outside the view's own rect is not that view's to answer, whatever its camera would map it to.
pub fn in_view_rect(self: const Stage, view: integer, sx: float, sy: float) -> boolean
Is this screen point inside the view's rect? An unbounded view holds every point, which is what keeps the unsplit stage a single code path.
pub fn hits_in(self: const Stage, view: integer, idx: integer, sx: float, sy: float) -> boolean
Does this node cover the screen point, as view maps it?
pub fn add_light(self: Stage, x: float, y: float, radius: float, colour: integer, power: float) -> integer
Add a light. Answers its index, for move_light.
pub fn move_light(self: Stage, i: integer, x: float, y: float)
Carry a light to a new place. DIRTIES the buffer, exactly as adding one does — a moving torch therefore re-packs every frame, which is the cost L2's composite pass exists to take off the per-sprite path.
pub fn light_count(self: const Stage) -> integer
How many lights the scene holds; the bound for move_light.
pub fn set_ambient(self: Stage, level: float)
How lit the world is before any light: 1.0 leaves the material untouched, so a game that never asks for light cannot be dimmed by this existing. Turn it down and the scene goes dark except where a light reaches.
pub fn ambient(self: const Stage) -> float
The level set_ambient left.
pub fn light_reach(l: const StageLight, px: float, py: float) -> float
What a light contributes at a point: linear in the radius, clamped at both ends.
⚠ LINEAR, 1 - d/r, and that is a design call. This phase's gate is hand-computed values, and a curve nobody can hand-compute is a gate nobody maintains. L2 owns the prettier curve, and its own gate says how: expectations GENERATED from the same constants the shader uses, so retuning is one edit.
pub fn lit_colour(self: const Stage, idx: integer) -> integer
The colour a node is actually drawn in: its material, multiplied by the light standing on its footprint.
⚠ ONE HOME. Both render paths and the instance packer read this rather than each folding the light in themselves — the shape P6 shipped a defect through, where one fact derived per path was validated only where the derivations happened to agree.
pub fn set_light_map(self: Stage, on: boolean)
Switch the composite pass on. Off, light is L1's per-sprite tint, which is one colour for a whole sprite; on, it is resolved per pixel.
pub fn light_map(self: const Stage) -> boolean
Is the composite pass on?
pub fn set_hud_layer(self: Stage, n: integer)
Layers at or above n draw AFTER the composite and take no light.
pub fn hud_layer(self: const Stage) -> integer
The first layer that draws after the composite, or HUD_NONE.
pub fn light_rgb_at(self: const Stage, wx: float, wy: float) -> Lit
The light standing at a world point, per channel.
⚠ THE ONE HOME both samplings read: L1 asks it at a sprite's origin, L2 asks it per pixel. One curve, one floor, one clamp — so the per-sprite and the per-pixel answers cannot disagree about the same point.
pub fn light_level_at(self: const Stage, wx: float, wy: float) -> float
The scalar level at a world point — the green channel of a white light, and the number the monotonic-and-floored invariant is stated about.
pub fn composite_light(self: const Stage, view: integer, cv: graphics::Canvas)
Multiply every pixel of the view's rect by the light standing there.
⚠ THE SCREEN IS MAPPED TO THE WORLD THROUGH THE WORLD PLANE — layer 0, the plane lights live on. A parallaxed background is therefore lit as though it stood on that plane, which is the honest simplification here: per-layer atmosphere is L3's question, not this pass's.
pub fn composite_shader_source() -> text
The composite's fragment shader. Public so a test can check it reads the falloff and the floor as UNIFORMS rather than carrying its own copy of them.
pub fn set_layer_fog(self: Stage, layer: integer, colour: integer, density: float)
Fade this layer toward colour. Density 0 is clear and changes nothing.
pub fn layer_fog_colour(self: const Stage, layer: integer) -> integer
The colour this layer fades toward. An unset layer reads the plain layer's, so a scene that never asked for fog cannot be tinted by this existing.
pub fn set_layer_blur(self: Stage, layer: integer, radius: integer)
Put this layer out of focus by radius pixels. 0 is sharp.
⚠ THE BLUR RUNS WHEN THE LAYER FINISHES, so it softens everything drawn up to and including it and NOTHING above — which is what makes a sharp foreground over a hazy distance possible, and what a fullscreen blur cannot do. Distant bands therefore blur together, which is what *distance is out of focus* means.
pub fn layer_blur(self: const Stage, layer: integer) -> integer
This layer's blur radius in pixels; 0 is sharp.
pub fn blur_region(cv: graphics::Canvas, x0: integer, y0: integer, x1: integer, y1: integer, radius: integer)
Box-blur a rectangle of the canvas in place.
⚠ SAMPLES ARE CLAMPED TO THE EDGE, never read as black. Counting an off-image sample as black loses light everywhere the kernel overhangs, and it shows as a dark frame around the picture — the classic bug this phase gates against. Clamping keeps a uniform field EXACTLY itself.