pub struct Background { /* private fields */ }Expand description
§Backgrounds: finite scenes, surfaces and parallax
Version 0.5 added static backgrounds built from native layers; 0.6 adds stroked shapes, tiles as sprites and backgrounds placed inside backgrounds. A surface is a single-palette grid of authored keys, separate from a sprite. A layer can combine surfaces, sprites (including textures and modifier chains) and tilemaps with different palettes. Backgrounds and surfaces never become implicit atlas entries. The existing screen, animation, texture and theme formats keep their behavior.
See examples/backgrounds.spritebox for a complete 320x180 scene and
examples/night-shift-background.spritebox for a static city companion to the
original examples/night_shift.rs animation.
The harbor source is 2,732 bytes with LF line endings. Its base export consumes 585,664 scene work units for a 57,600-pixel viewport and seven native layers. The static city companion is 929 bytes, with 288,000 base-export scene work units for a 480x300 viewport and one native layer. These are reproducible input/output and work-counter sizes, not timing promises. The original Rust demo still owns its 96-frame rainy-city animation; the companion moves representative sky, buildings, moon, windows, neon and railway geometry into declarative text.
§Rust API
use spritebox::{BackgroundRenderOptions, Document, ExportOptions};
let document = Document::parse("\
palette p
a #234a65
surface sky 8x6 palette=p
rect fill at=0,0 size=8x6 key=a
background land 8x6
layer sky 8x6 parallax=0/1,0/1
place surface sky at=0,0
")?;
let background = document.background("land").unwrap();
let image = background.render(&document, &BackgroundRenderOptions { camera: (-4, 0) })?;
assert_eq!(image.width(), 8);
let export = document.export(&ExportOptions::default())?;
assert_eq!(export.backgrounds[0].layers[0].image.height(), 6);Document::surfaces, surface, backgrounds and background inspect immutable
objects. Surface::key returns None for EMPTY or out-of-bounds cells;
Surface::profile(name).heights() exposes original y boundaries.
Surface::builder paints typed Primitive, Gradient and ProfileOptions
operations within explicit limits, and strokes rectangles and ellipses. Background::builder adds named layers,
SourceRef placements and weighted scatter groups using the same generator as
text parsing. finish returns an immutable object. Document::with_surface and
with_background insert validated objects into a new document; references must
resolve in that document. A background can also render before insertion.
render_with, export_with, export_all_themes_with and preview_with accept
explicit limits. Rendering can fail on storage or work exhaustion.
§Coordinates and layer composition
background harbor 320x180 seed=7
layer town 128x96 at=-8,84 repeat=x parallax=1/4,0/1 clip=0,0,128,96
place surface buildings at=0,0
place sprite house:flip_x at=24,96 anchor=bottom-left
place tilemap docks at=64,0 crop=0,0,64,48Background dimensions are viewport pixels; layer dimensions are the native
canvas and repeat period. Sizes must be positive. Positions are signed i32,
with +x right and +y down. Native layer dimensions fit signed coordinates.
at defaults to 0,0 on layers and is required on ordinary placements.
Names use ASCII letters, digits, _ and -; surface/background namespaces are
distinct. Forward and imported references work. Screens do not belong in a
background; another background does, as below. Core APIs perform no filesystem I/O.
crop=x,y,w,h is a positive in-bounds source rectangle. Crop is applied first,
then the anchor: top-left subtracts nothing, bottom-left subtracts the cropped
height, and center subtracts floor(width/2), floor(height/2). Placements clip to
the native layer. The optional in-bounds layer clip is baked after composing
items. All rectangle bounds are half-open.
§Backgrounds inside backgrounds
background board 256x256
layer ground 256x256
place surface grass at=0,0
layer units 256x256
place sprite tower at=40,60
background shop 600x270
layer board 600x270
place background board at=172,7
layer hud 600x270
place background board layer=units at=8,8 crop=0,0,128,128![]()
place background NAME [layer=LAYER] at=X,Y [crop=...] [anchor=...] draws another
background into a layer. Without layer=, the source is the placed background’s
flat image at camera 0,0; with it, only that layer, as it appears in that flat
image (its origin, repeat and clip applied, at camera 0,0). Either way the source
has the placed background’s size, which crop and anchor refer to. The placed
background is drawn as an image, with the usual source-over, so pixels equal a copy
of its placements in a layer with nothing under them. It is never moved by the
outer camera’s parallax inside the outer layer.
Placed backgrounds may be defined later in the file or imported. A circle, such as
a placing b placing a, is an error at the placement that closes it, naming the
chain a -> b -> a; so is nesting deeper than Limits::max_nesting. An unknown
layer is an error that lists the background’s layers. Rendering a placed background
spends the storage and work budget of the background that places it, every time it
is placed. Themes recolor placed backgrounds too. Exports keep backgrounds in
written order. In Rust, place SourceRef::Background { name, layer }.
Items draw in source order onto transparent straight RGBA8 native layers. Then
layers draw in source order over the transparent viewport using Color::over.
This two-stage grouping is part of the format: integer alpha rounding makes
directly blending every item onto the viewport observably different.
repeat=none|x|y|both defaults to none. parallax=NX/DX,NY/DY defaults to
1/1,1/1; numerators are signed i32 and denominators positive u32. Fractions
are reduced, and zero becomes 0/1. For output pixel (x,y), layer origin (ox,oy)
and camera (cx,cy), sample:
u = x - ox + floor(cx * NX / DX)
v = y - oy + floor(cy * NY / DY)Repeating axes use rem_euclid(coordinate, native_size); other axes return
transparent outside the native bounds. Camera x=-1 with parallax 1/4 samples
an offset of -1. Transparent margins retain their place in the repeat period.
Camera movement samples the same generated keys; it does not regenerate noise,
scatter, profiles or dithering. There is no zoom, rotation, tick or velocity.
§Painting surfaces
surface buildings 128x96 palette=world seed=0
rect wall at=8,32 size=40x64 key=b
line aerial from=24,10 to=24,31 key=k
polygon roof key=r points=4,32 28,12 52,32
ellipse moon at=84,8 size=24x24 key=m
ellipse range at=60,40 size=33x21 key=k stroke=1
rect frame at=0,0 size=128x96 key=k stroke=2All operations are named; names are unique within their owning surface. Operations overwrite a covered key, including a key whose current color is transparent. They do not blend colors. EMPTY is distinct from every palette key, and changing alpha cannot reveal an overwritten key.
Rectangles fill half-open boxes. Lines include both endpoints, normalize them
lexicographically, then use the dominant axis with nearest-integer minor offsets:
floor((2*i*abs(minor)+abs(major))/(2*abs(major))). Ties round upward in the
minor direction. Clipping preserves the original phase and visits only the
visible dominant-axis range, even for extreme off-canvas endpoints.
Polygons sample pixel centers with even-odd fill. Vertical edge intervals include the lower y and exclude the upper y. At least three distinct vertices and nonzero signed area are required; repeated vertices, intersections and overlapping edges are errors. Reversing winding preserves coverage. Ellipses sample pixel centers inside a positive bounding box using exact wide integer arithmetic. None of these operations adds antialiasing or intermediate RGB values.
![]()
stroke=N (N >= 1) on a rect or an ellipse paints only the band N pixels wide
inside the shape’s edge: exactly the pixels of the filled shape minus those of the
same shape shrunk by N on every side (at + N, size - 2N), with the same
coverage rules, so a filled shape and its stroke meet exactly. Keys inside the ring
are kept. When the shape is no more than 2N wide or high, the shrunk shape is empty
and the whole shape is painted. Lines and polygons have no stroke=. Rust has
SurfaceBuilder::stroke.
§Key gradients
surface sky 320x180 palette=world
gradient dawn axis=y mode=blend dither=bayer4
stop 0 n
stop 72 p
stop 179 yaxis=x|y; mode=bands|blend; dither=none|bayer2|bayer4|bayer8, default none.
Stops use single palette keys at strictly increasing in-bounds pixel positions.
A single stop fills uniformly. Before/after the stop range, use the endpoint key.
Bands chooses the last stop at or before the coordinate and accepts only no
dithering. Blend computes floor((coordinate-a)*65535/(b-a)) and uses the existing
Dither::pick(t,x,y,2) to choose an endpoint key. Exact stops select their own key.
No RGB interpolation, luminance ordering or color distance enters selection.
Dither coordinates are surface-local, so crop/camera do not shift the pattern.
§Profiles and deterministic seeds
surface terrain 320x180 palette=world seed=7
profile ridge key=m baseline=132 amplitude=44 cell=32 octaves=3 roughness=60 seed=11Profiles reuse existing wrapped Noise with width=surface width and height=cell,
sampling value(x,0) without stretched normalization:
height[x] = baseline - floor(amplitude * value(x,0)/65535).
Require 0 <= amplitude <= baseline <= surface_height, cell divisibility and
the existing octave/roughness rules. Fill key at y >= height[x]. Heights remain
available after later paint operations. A bottom boundary equal to the surface
height leaves that column empty. Large amplitude gives mountain ridges; lower
amplitude and roughness give hills/dunes. These are parameters of the same value
noise, not separate algorithms.
scene_seed hashes each identity component as u64 little-endian UTF-8 byte length
followed by its bytes, then each u64 seed in little-endian order. FNV-1a-64 starts
at 0xcbf29ce484222325, XORs each byte and multiplies with wrapping arithmetic by
0x100000001b3. Apply the SplitMix64 finalizer: xor-shift 30/multiply
0xbf58476d1ce4e5b9, xor-shift 27/multiply 0x94d049bb133111eb, xor-shift 31.
Profile identity is ["profile", surface, operation]; scatter identity is
["scatter", background, layer, operation]. Seeds are [owner_seed, op_seed],
default zero. Names, not paths, positions or definition order, scope randomness.
Pinned seed vectors, hexadecimal:
| Identity | Seeds | Result |
|---|---|---|
| profile / s / ridge | 9, 3 | 4b19d0a4f3e342ed |
| scatter / scene / hills / stars | 11, 4 | ae69762aa276d3ae |
| profile / Ω / ridge | 0, 0 | 1b8b20325fd5a34c |
The last vector documents UTF-8 hashing; authored definition names still use the ASCII name grammar. Existing texture seeds are unchanged.
§Scatter
background harbor 320x180 seed=7
layer trees 320x180 repeat=x
scatter grove count=12 spacing=10 attempts=512 seed=4 baseline=terrain(ridge) mask=terrain(m)
variant tree weight=3
variant tree:flip_x weight=1Give exactly one of count=N or density=P/Q, where 0 <= P <= Q and Q > 0.
Density floors eligible anchors * P/Q, before spacing. Ordinary candidates are
native integer pixels; profile candidates use x and the authored height[x].
Masks compare the final raw surface key, independently of alpha, at the candidate
anchor. For a profile boundary, test mask y clamped to 0..H-1. Mask/profile surface
dimensions must equal native layer dimensions. EMPTY never matches.
Every accepted sprite uses bottom-left anchor. Spacing is minimum Euclidean
anchor distance within that group, using shortest wrapped distances on repeated
axes. Duplicates are forbidden even with spacing=0. Candidate rejection never
changes subsequent random draws. For attempt i and lane j, finalize
seed + i*0x9e3779b97f4a7c15 + j*0xd1b54a32d192ed03 (wrapping u64), then map to
range N with the high 64 bits of the u128 product random*N. Lanes 0/1/2 select
x/y/weighted variant. Weights are positive u32, summed in checked u64; weighted
intervals follow authored order. This finite mapping uses no rejection loop.
Attempts default to max(64,32*target); an explicit value must be positive. Target zero succeeds without attempts. Failure to reach the exact target is an error showing target, accepted and attempts; tune count, spacing, attempts or mask. No partial scatter output is returned. Scatter is static decoration, not physics or alpha collision detection.
§Export and reconstruction
ExportOptions.backgrounds selects a name, camera and BackgroundMode:
None, Flat, Layers or Both (default). Ordinary assets remain exported.
Global background-definition and local layer ordinals determine names before
selection: background-0000.png, background-0000-layer-0000.png. Theme files
append -theme-0000 before .png; theme order is the document’s order. Names do
not become paths. Full output paths are checked case-insensitively before writes.
Root metadata stays format version 1; documents without exported backgrounds
keep their old shape. The optional backgrounds object has schema:1,
units:"pixel" and ordered items. Each item stores name, size:[W,H], optional
flat:{image,camera:[x,y]} and optional layers. A layer stores name, image,
size, origin, repeat and parallax:{x:{num,den},y:{num,den}}. Array order is
draw order. Omitted modes produce no dangling filenames. Themes have the same
background section inside their existing theme entry.
Layer PNGs are native straight RGBA8 after crop, anchoring, item composition and
clip. They are not viewport-padded, trimmed, extruded or camera-shifted. To
reconstruct: clear a viewport to transparent; for each layer, apply the exact
floor/remainder formula above at each viewport pixel, fetch its native pixel or
transparent, and source-over onto the viewport using Color::over. Do not reapply
clip/crop. tests/backgrounds.rs contains an independent implementation and exact
metadata/PNG/key/profile/scatter fixtures under tests/fixtures/backgrounds.
§CLI, preview and lint
spritebox check examples/backgrounds.spritebox --json
spritebox render examples/backgrounds.spritebox --out out/backgrounds --theme all
spritebox render examples/backgrounds.spritebox --out out/camera --background harbor --camera -64,0 --background-mode both --lintBackground selectors/camera/mode apply only to directory render, and reject
duplicate flags or unknown names. --max-side remains the atlas/tileset limit;
background PNGs use image limits and retain native dimensions regardless of
sprite preview --scale. --background-scale N (2..=8) also writes every exported
background PNG enlarged N times by pixel replication, as STEM@Nx.png beside
STEM.png (for example background-0000@3x.png); native files and metadata are
byte-identical with and without it. All outputs and the preview validate before
first write.
Background thumbnails use divisor=max(1,ceil(W/320),ceil(H/180)), output
ceil(W/divisor) by ceil(H/divisor), and nearest samples at coordinate*divisor.
They are labelled with native size and camera and never enlarged by --scale.
Intermediate surfaces do not each get a preview entry. Generator/mask keys count
as palette uses. Sprite-only heuristics do not analyze full generated backgrounds.
background_clipped reports a fully clipped placement with source position and
imported filename. Existing file/definition allows, strict/deny and warning limits
apply. Parse, generation and resource failures remain errors.
§Budgets and compatibility
Limits retains its existing image/sprite/map/nesting/definition bounds and adds:
| Limit | Default | Accounted resources |
|---|---|---|
| max_scene_commands | 4096 | Operations, layers, placements, vertices, stops, variants |
| max_scene_pixels | 1610241024 | Surface keys, height entries, shared source key buffers, placement records, retained/temporary images |
| max_scene_work | 6410241024 | Raster samples, edge tests, noise octaves, proposals and spacing comparisons |
| max_scatter_attempts | 1,000,000 | Candidate attempts across document generation |
Counters are shared across children for each top-level parse/render/export.
Transforms reserve source/output/temporary buffers before execution and charge a
conservative 16 work units per source/output key cell. Retained source buffers are
deduplicated by shared ownership. Export across themes shares one work/storage
budget. CLI export and preview share remaining budget. Platform byte bounds are
checked before allocating, including 32-bit WASM. Raise limits explicitly for
trusted larger inputs. Struct literals can preserve defaults with ..Default::default().
Backgrounds preserve safe Rust, zero production dependencies, MSRV 1.85 and the in-memory WASM core. Cyclic cloud/rain/water motion, frame timelines, world streaming, automatic background atlasing, new noise algorithms and full Night Shift animation migration are outside 0.5.