create.image¶
Images and animation: Image, PixelBuffer, Animation and Animator.
Image¶
struct Image
An RGBA image, row-major, 8 bits per channel: an asset, never changed once made.
Decoded once at load — Image.load picks a BMP, PNG or JPEG decoder by
file extension — and blitted many times afterwards. Image(buffer^)
takes over the pixels of a PixelBuffer, the way to build one pixel by
pixel; Image.solid and Image.from_rgba build one without a file, and
resize returns a resampled copy. pixel reads one pixel. Since nothing
changes an image, a backend's cached copy of one is never stale.
Deliberately not a render type: raster.blit_image takes a pixel pointer
with a width and a height rather than this struct, so the image decoders
stay out of the render path and the rasteriser is written against no layout
but its own.
Implements Copyable, Deinitable, ImplicitlyCopyable, Movable.
Constructors¶
@staticmethod
def __init__(out self, var pixels: PixelBuffer) raises
An image of pixels, which it takes over without copying.
width¶
def width(self) -> Int
The image's width in pixels.
height¶
def height(self) -> Int
The image's height in pixels.
solid¶
@staticmethod
def solid(width: Int, height: Int, color: Color) raises -> Self
A width x height image every pixel of which is color.
from_rgba¶
@staticmethod
def from_rgba(width: Int, height: Int, data: List[UInt8]) raises -> Self
A width x height image of data: RGBA bytes, row by row from the top-left corner. Bytes past width * height * 4 are ignored.
Raises if either dimension is negative or data is too short.
pixel¶
def pixel(self, x: Int, y: Int) -> Color
The pixel in column x of row y, counted from the top-left corner — image coordinates, unlike the canvas's. Outside the image it is Color.TRANSPARENT.
resize¶
def resize(self, width: Int, height: Int) raises -> Self
A copy of this image at width x height, nearest-neighbour sampled. This image is unchanged; the copy is a new image, with its own identity.
load¶
@staticmethod
def load(path: String) raises -> Self
Load an image file. Supports BMP, PNG, and JPEG.
A relative path is resolved against the directory of the calling
source file, not the working directory, so
Image.load("assets/hero.png") finds the file beside the program
wherever it is run from.
PixelBuffer¶
struct PixelBuffer
Pixels being built one at a time, to become an Image.
The mutable half of an image: set writes a pixel, pixel reads one
back, and Image(buffer^) takes the pixels over without copying them.
An Image is never changed after that, so this is where an image built
pixel by pixel is built.
Image coordinates, as Image.pixel: column x of row y, counted from
the top-left corner, y down — not the canvas's screen space. Off the
buffer, reads are transparent and writes do nothing. A new buffer is
transparent.
Implements Deinitable, Movable.
Constructors¶
@staticmethod
def __init__(out self, width: Int, height: Int)
width¶
def width(self) -> Int
The buffer's width in pixels.
height¶
def height(self) -> Int
The buffer's height in pixels.
pixel¶
def pixel(self, x: Int, y: Int) -> Color
The pixel in column x of row y, counted from the top-left corner. Outside the buffer it is Color.TRANSPARENT.
set¶
def set(mut self, x: Int, y: Int, color: Color)
Replace the pixel in column x of row y, counted from the top-left corner. Outside the buffer it does nothing.
Animation¶
struct Animation
An ordered sequence of frames and the rate they play at.
An Animation is the asset, not the playhead -- it holds the frames
and how fast they are meant to run, and never mutates. Animator
walks it. The rate lives here because it is a property of the artwork: a
run cycle and an idle cycle are rendered for different speeds.
Frames are cut into owned Images at construction, so a sheet or a folder
is paid for once and every render afterwards is a plain blit.
Copying one is cheap, like copying an Image: the copies share the
frames, so several animators can hold the same animation.
Implements Copyable, Deinitable, ImplicitlyCopyable, Movable.
Fields¶
fps: Float64
Constructors¶
@staticmethod
def __init__(out self, var frames: List[Image], fps: Float64 = 12) raises
Build an animation from frames already in memory.
Raises on an empty frame list: an animation with no frames has no valid frame index, so every consumer would have to guard against it.
frame_count¶
def frame_count(self) -> Int
The number of frames.
frame¶
def frame(self, index: Int) -> Image
Frame index, from 0 to frame_count() - 1. Cheap: an Image copy shares its pixels.
frame_duration¶
def frame_duration(self) -> Float64
Seconds each frame is held.
from_sheet¶
@staticmethod
def from_sheet(
sheet: Image,
frame_width: Int,
frame_height: Int,
start: Int = Int(0),
count: Int = Int(0),
fps: Float64 = 12,
) raises -> Self
Cut frames out of a sprite sheet, left to right then top to bottom.
Cells are numbered row-major from zero, so one sheet holding idle on row 0 and run on row 1 yields two animations without a sheet type:
var idle = Animation.from_sheet(sheet, 32, 32, start=0, count=4)
var run = Animation.from_sheet(sheet, 32, 32, start=4, count=6)
count = 0 means every remaining cell. Raises when the sheet is not a
whole number of cells wide or tall, or when the window runs past the
last cell -- a silently truncated animation is harder to notice than a
failed load.
from_folder¶
@staticmethod
def from_folder(path: String, fps: Float64 = 12) raises -> Self
Load every image in a directory as a frame, in natural number order.
var run = Animation.from_folder("assets/run")
A relative path is resolved against the directory of the calling
source file, as Image.load's is.
Names are ordered by the number they end in, not by string comparison,
so frame_2 comes before frame_10 -- the ordering an author means
and the one zero-padding is usually a workaround for. Files that are
not BMP, PNG or JPEG are skipped; a directory with no images at all
raises rather than yielding an empty animation.
Animator¶
struct Animator
The playhead over one Animation.
An animation is the artwork and never moves; an animator is the frame a particular entity is on right now, so an entity owns one and several can share the same animation. Ticked once per frame with the frame delta:
def update(mut self, mut context: Context, mut canvas: Canvas) raises:
self.animator.update(context.time.delta)
Constructed stopped on frame 0 -- nothing advances until play, loop
or use. Two kinds of call drive it:
- Commands, for something that just happened (a key, a hit):
playruns the animation once and holds the last frame,loopwraps forever, and both always start from frame 0, with or without a new animation, asTween.playandAudio.playalways start.pausefreezes where it is andresumecontinues from there;stoprewinds. use, for what should be showing now, called every frame from the branch that decides it. Naming the animation already held changes nothing, so it never rewinds a running loop or undoes apause.
if context.keyboard.pressed(Key.SPACE):
self.animator.play(self.spin) # a command: starts every time
elif not self.spinning:
self.animator.use(self.run if moving else self.idle) # a state
Implements Copyable, Deinitable, Movable, Writable.
Constructors¶
@staticmethod
def __init__(out self, var animation: Animation)
Hold animation, stopped on frame 0.
An animator always has an animation, so there is no empty state to guard against when rendering.
animation¶
def animation(self) -> Animation
The animation being played, sharing its frames. Switch with use, play or loop.
frame_index¶
def frame_index(self) -> Int
The frame showing, counted from 0.
use¶
def use(mut self, var animation: Animation)
Show animation, looping: the call to make every frame from the branch that decides which animation applies.
if self.velocity.x != 0.0:
self.animator.use(self.run)
else:
self.animator.use(self.idle)
Naming the animation already held, once it has been started, does
nothing at all: a loop keeps its place, a pause holds and a
finished play stays on its last frame. Otherwise -- another
animation, or this one stopped -- it starts animation looping from
frame 0. A one-shot is a command, not a state: play it when the
thing that triggers it happens.
play¶
def play(mut self)
Play once from frame 0, holding the last frame at the end.
def play(mut self, var animation: Animation)
Switch to animation and play it once from frame 0 -- restarting it if it is the one already held.
loop¶
def loop(mut self)
Play from frame 0, wrapping to the start forever.
def loop(mut self, var animation: Animation)
Switch to animation and loop it from frame 0 -- restarting it if it is the one already held. To keep a running loop's place, use it.
pause¶
def pause(mut self)
Freeze on the current frame. resume continues from here.
resume¶
def resume(mut self)
Continue a paused animation without rewinding.
stop¶
def stop(mut self)
Halt and rewind to frame 0.
update¶
def update(mut self, dt: Float64)
Advance the playhead by dt seconds. Call once per frame.
Frames are advanced in a loop rather than by one step, so a long frame skips ahead instead of falling behind the animation's own clock.
is_playing¶
def is_playing(self) -> Bool
True while the playhead is advancing -- false when stopped, paused or finished.
is_finished¶
def is_finished(self) -> Bool
True once a play reached its last frame. Never true for loop.