Skip to content

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): play runs the animation once and holds the last frame, loop wraps forever, and both always start from frame 0, with or without a new animation, as Tween.play and Audio.play always start. pause freezes where it is and resume continues from there; stop rewinds.
  • 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 a pause.
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.