ludic/runtime/native/atlas.ludic

410 lines
18 KiB
Text

# ============================================================================
# atlas.ludic — a namespaced spritesheet / atlas API (#81).
#
# Sprite loading used to be a bare png_load("assets/sprites/floor0.png") — one
# file per 16x16 sprite, and no way to load one sheet and address a cell by grid
# coords or by name. This adds:
#
# Sprite.sheet(path, cellw, cellh) -> handle load a sheet, remember its grid
# Sprite.cell(sheet, col, row) -> id one cell, addressed by grid coords
# Sprite.cell_span(sheet, col, row, cols, rows) a sprite spanning >1 cell (tall/wide)
# Sprite.define(name, sheet, col, row) -> id name a cell for later lookup
# Sprite.named(name) -> id look a named sprite up
# Sprite.draw(id, x, y) / draw_scaled(id,x,y,sc) blit an atlas sprite
# Sprite.width(id) / height(id) its pixel size
# Assets.image(path) -> id a whole file as one sprite
# Assets.load(path) -> id alias of Assets.image
# Assets.get(name) -> id alias of Sprite.named
#
# It stands on the variable-size image loader in image.ludic (rt_image_load keeps
# the whole decoded PNG; a cell is a sub-rect of it), so a sprite is NOT restricted
# to the 16x16 sprite table — a cell (and a multi-cell span) can be any size. Draws
# go through rt_put_px, so the camera, zoom (#78) and clip all apply, exactly like
# Screen.sprite. Spliced on demand when a program uses Sprite.sheet / Assets.*.
# ============================================================================
const ATLAS_MAX_SHEET: int = 32
const ATLAS_MAX_SPR: int = 1024
const ATLAS_MAX_NAME: int = 512
export state RtAtlasState {
at_sheet_img: words = null # image id per sheet
at_sheet_cw: words = null # cell width per sheet
at_sheet_ch: words = null # cell height per sheet
at_nsheet: int = 0
at_spr_sheet: words = null # owning sheet per atlas sprite
at_spr_sx: words = null # source x/y (px) of the sub-rect
at_spr_sy: words = null
at_spr_w: words = null # sub-rect width/height (px)
at_spr_h: words = null
at_nspr: int = 0
at_name_str: pointers = null # name per named sprite
at_name_id: words = null # atlas id per name
at_nname: int = 0
at_q_name: pointers = null # name to register the loaded sprite under
at_q_path: pointers = null # file path to load
at_q_n: int = 0 # how many enqueued
at_q_pos: int = 0 # how many loaded so far
at_q_announced: bool = false
at_font_name: pointers = null
at_font_id: words = null
at_font_n: int = 0
}
# #82 — an incremental preload queue: enqueue named image files, then load a
# bounded number per frame (Assets.pump) so a loading scene stays responsive and
# a game only enters play once Assets.ready(). Deterministic: the same enqueue +
# pump order loads the same assets in the same order on every run.
import "audio.ludic" # the preload queue feeds .wav/.mp3 into the sound bank
const ATLAS_MAX_QUEUE: int = 512
function atlas_init(rt_atlas_st: mut RtAtlasState) -> void {
if rt_atlas_st.at_sheet_img != null { return }
rt_atlas_st.at_sheet_img = words(ATLAS_MAX_SHEET)
rt_atlas_st.at_sheet_cw = words(ATLAS_MAX_SHEET)
rt_atlas_st.at_sheet_ch = words(ATLAS_MAX_SHEET)
rt_atlas_st.at_spr_sheet = words(ATLAS_MAX_SPR)
rt_atlas_st.at_spr_sx = words(ATLAS_MAX_SPR)
rt_atlas_st.at_spr_sy = words(ATLAS_MAX_SPR)
rt_atlas_st.at_spr_w = words(ATLAS_MAX_SPR)
rt_atlas_st.at_spr_h = words(ATLAS_MAX_SPR)
rt_atlas_st.at_name_str = pointers(ATLAS_MAX_NAME) # a pointer (8 bytes) per name slot
rt_atlas_st.at_name_id = words(ATLAS_MAX_NAME)
rt_atlas_st.at_q_name = pointers(ATLAS_MAX_QUEUE)
rt_atlas_st.at_q_path = pointers(ATLAS_MAX_QUEUE)
}
# register (name -> atlas id) in the name table so Assets.get / Sprite.named find it.
function atlas_register_name(rt_atlas_st: mut RtAtlasState, name: pointer, id: int) -> void {
atlas_init(rt_atlas_st)
if rt_atlas_st.at_nname >= ATLAS_MAX_NAME { return }
rt_atlas_st.at_name_str[rt_atlas_st.at_nname] = name
rt_atlas_st.at_name_id[rt_atlas_st.at_nname] = id
rt_atlas_st.at_nname += 1
}
# load a spritesheet PNG whose cells are cw x ch px; returns a sheet handle (>= 0).
function atlas_sheet(rt_atlas_st: mut RtAtlasState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: pointer, cw: int, ch: int) -> int {
atlas_init(rt_atlas_st)
if rt_atlas_st.at_nsheet >= ATLAS_MAX_SHEET { return -1 }
let img = rt_image_load(rt_image_st, rt_inflate_st, path)
if img < 0 { return -1 }
let h = rt_atlas_st.at_nsheet
rt_atlas_st.at_sheet_img[h] = img
rt_atlas_st.at_sheet_cw[h] = cw
rt_atlas_st.at_sheet_ch[h] = ch
rt_atlas_st.at_nsheet += 1
return h
}
# register an atlas sprite for a sub-rect (px) of a sheet's image.
function atlas_make(rt_atlas_st: mut RtAtlasState, sheet: int, sx: int, sy: int, w: int, h: int) -> int {
atlas_init(rt_atlas_st)
if rt_atlas_st.at_nspr >= ATLAS_MAX_SPR { return -1 }
let id = rt_atlas_st.at_nspr
rt_atlas_st.at_spr_sheet[id] = sheet
rt_atlas_st.at_spr_sx[id] = sx
rt_atlas_st.at_spr_sy[id] = sy
rt_atlas_st.at_spr_w[id] = w
rt_atlas_st.at_spr_h[id] = h
rt_atlas_st.at_nspr += 1
return id
}
# one cell (col, row) of a sheet, addressed by grid coords.
function atlas_cell(rt_atlas_st: mut RtAtlasState, sheet: int, col: int, row: int) -> int {
if (sheet < 0) or (sheet >= rt_atlas_st.at_nsheet) { return -1 }
let cw = rt_atlas_st.at_sheet_cw[sheet]
let ch = rt_atlas_st.at_sheet_ch[sheet]
return atlas_make(rt_atlas_st, sheet, col * cw, row * ch, cw, ch)
}
# a sprite spanning cols x rows cells from (col, row) — some sprites cover more
# than one cell (a tall character, a wide object).
function atlas_cell_span(rt_atlas_st: mut RtAtlasState, sheet: int, col: int, row: int, cols: int, rows: int) -> int {
if (sheet < 0) or (sheet >= rt_atlas_st.at_nsheet) { return -1 }
let cw = rt_atlas_st.at_sheet_cw[sheet]
let ch = rt_atlas_st.at_sheet_ch[sheet]
return atlas_make(rt_atlas_st, sheet, col * cw, row * ch, cols * cw, rows * ch)
}
# name a cell so a game can look it up by name; returns the atlas id.
function atlas_define(rt_atlas_st: mut RtAtlasState, name: pointer, sheet: int, col: int, row: int) -> int {
atlas_init(rt_atlas_st)
let id = atlas_cell(rt_atlas_st, sheet, col, row)
if id < 0 { return -1 }
if rt_atlas_st.at_nname < ATLAS_MAX_NAME {
rt_atlas_st.at_name_str[rt_atlas_st.at_nname] = name
rt_atlas_st.at_name_id[rt_atlas_st.at_nname] = id
rt_atlas_st.at_nname += 1
}
return id
}
# look up a named sprite's atlas id (names compare by content), or -1.
function atlas_named(rt_atlas_st: mut RtAtlasState, name: pointer) -> int {
atlas_init(rt_atlas_st)
var i = 0
while i < rt_atlas_st.at_nname {
if rt_atlas_st.at_name_str[i] == name { return rt_atlas_st.at_name_id[i] }
i += 1
}
return -1
}
# load a whole image file as one atlas sprite (a 1x1 sheet the size of the image).
function atlas_image(rt_atlas_st: mut RtAtlasState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: pointer) -> int {
atlas_init(rt_atlas_st)
if rt_atlas_st.at_nsheet >= ATLAS_MAX_SHEET { return -1 }
let img = rt_image_load(rt_image_st, rt_inflate_st, path)
if img < 0 { return -1 }
let w = rt_image_st.img_w[img]
let h = rt_image_st.img_h[img]
let sh = rt_atlas_st.at_nsheet
rt_atlas_st.at_sheet_img[sh] = img
rt_atlas_st.at_sheet_cw[sh] = w
rt_atlas_st.at_sheet_ch[sh] = h
rt_atlas_st.at_nsheet += 1
return atlas_make(rt_atlas_st, sh, 0, 0, w, h)
}
# blit an atlas sprite at (dx, dy) through rt_put_px (camera / zoom / clip apply).
# Only sufficiently-opaque pixels are drawn (the sprite's transparent border is
# skipped), matching Screen.sprite.
function atlas_draw(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int) -> void {
atlas_init(rt_atlas_st)
if (id < 0) or (id >= rt_atlas_st.at_nspr) { return }
let sheet = rt_atlas_st.at_spr_sheet[id]
let img = rt_atlas_st.at_sheet_img[sheet]
let iw = rt_image_st.img_w[img]
let ih = rt_image_st.img_h[img]
let s: words = rt_image_st.img_px[img]
let sx = rt_atlas_st.at_spr_sx[id]
let sy = rt_atlas_st.at_spr_sy[id]
let w = rt_atlas_st.at_spr_w[id]
let h = rt_atlas_st.at_spr_h[id]
var y = 0
while y < h {
let srcy = sy + y
if (srcy >= 0) and (srcy < ih) {
var x = 0
while x < w {
let srcx = sx + x
if (srcx >= 0) and (srcx < iw) {
let px = s[srcy * iw + srcx]
if ((px >> 24) & 255) >= 128 { rt_put_px(rt_core_st, dx + x, dy + y, px & 16777215) }
}
x += 1
}
}
y += 1
}
}
# blit an atlas sprite scaled by an integer factor (through rt_fill_rect blocks).
function atlas_draw_scaled(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int, sc: int) -> void {
atlas_init(rt_atlas_st)
if (id < 0) or (id >= rt_atlas_st.at_nspr) { return }
if sc < 1 { return }
let sheet = rt_atlas_st.at_spr_sheet[id]
let img = rt_atlas_st.at_sheet_img[sheet]
let iw = rt_image_st.img_w[img]
let ih = rt_image_st.img_h[img]
let s: words = rt_image_st.img_px[img]
let sx = rt_atlas_st.at_spr_sx[id]
let sy = rt_atlas_st.at_spr_sy[id]
let w = rt_atlas_st.at_spr_w[id]
let h = rt_atlas_st.at_spr_h[id]
var y = 0
while y < h {
let srcy = sy + y
if (srcy >= 0) and (srcy < ih) {
var x = 0
while x < w {
let srcx = sx + x
if (srcx >= 0) and (srcx < iw) {
let px = s[srcy * iw + srcx]
if ((px >> 24) & 255) >= 128 { rt_fill_rect(rt_core_st, dx + x * sc, dy + y * sc, sc, sc, px & 16777215) }
}
x += 1
}
}
y += 1
}
}
# blit an atlas sprite with the full Sprite-component treatment (#90): integer scale,
# horizontal flip, and a solid tint (non-zero = every opaque pixel in that colour).
# This is what the engine sprite-render system calls for `Sprite { atlas: 1 }`.
function atlas_draw_ex(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int, sc0: int, flip: int, tint: int) -> void {
atlas_init(rt_atlas_st)
if (id < 0) or (id >= rt_atlas_st.at_nspr) { return }
var sc = sc0
if sc < 1 { sc = 1 }
let sheet = rt_atlas_st.at_spr_sheet[id]
let img = rt_atlas_st.at_sheet_img[sheet]
let iw = rt_image_st.img_w[img]
let ih = rt_image_st.img_h[img]
let s: words = rt_image_st.img_px[img]
let sx = rt_atlas_st.at_spr_sx[id]
let sy = rt_atlas_st.at_spr_sy[id]
let w = rt_atlas_st.at_spr_w[id]
let h = rt_atlas_st.at_spr_h[id]
var y = 0
while y < h {
let srcy = sy + y
if (srcy >= 0) and (srcy < ih) {
var x = 0
while x < w {
var rx = x
if flip != 0 { rx = w - 1 - x }
let srcx = sx + rx
if (srcx >= 0) and (srcx < iw) {
let px = s[srcy * iw + srcx]
if ((px >> 24) & 255) >= 128 {
var col = px & 16777215
if tint != 0 { col = tint & 16777215 }
if sc == 1 { rt_put_px(rt_core_st, dx + x, dy + y, col) }
else { rt_fill_rect(rt_core_st, dx + x * sc, dy + y * sc, sc, sc, col) }
}
}
x += 1
}
}
y += 1
}
}
# a strip of `count` frames starting at (col,row), each `rows` cells tall, registered
# as consecutive ids so `first + SpriteAnim.frame` addresses the current frame.
function atlas_strip(rt_atlas_st: mut RtAtlasState, sheet: int, col: int, row: int, count: int, rows: int) -> int {
var first = -1
var i = 0
while i < count {
let id = atlas_cell_span(rt_atlas_st, sheet, col + i, row, 1, rows)
if first < 0 { first = id }
i += 1
}
return first
}
# an atlas sprite's pixel size — handy for centering / layout.
function atlas_width(rt_atlas_st: mut RtAtlasState, id: int) -> int { atlas_init(rt_atlas_st); if (id < 0) or (id >= rt_atlas_st.at_nspr) { return 0 }; return rt_atlas_st.at_spr_w[id] }
function atlas_height(rt_atlas_st: mut RtAtlasState, id: int) -> int { atlas_init(rt_atlas_st); if (id < 0) or (id >= rt_atlas_st.at_nspr) { return 0 }; return rt_atlas_st.at_spr_h[id] }
# Sprite.draw_meter: `value` of `max` as a row of icons — full, half or empty per
# `per_icon` points (hearts, stars, ammo pips), `spacing` px apart
function atlas_draw_meter(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, x: int, y: int, value: int, max: int, per_icon: int, spacing: int, full: int, half: int, empty: int) -> void {
if per_icon <= 0 { return }
var i = 0
while i < max / per_icon {
var id = empty
if value >= i * per_icon + per_icon / 2 { id = half }
if value >= (i + 1) * per_icon { id = full }
atlas_draw(rt_atlas_st, rt_core_st, rt_image_st, id, x + i * spacing, y)
i += 1
}
}
# Assets.enqueue_dir: every file of a directory, named by its file name without the
# extension (assets/audio/hit.wav -> "hit"); the queue sorts by extension as usual
function assets_enqueue_dir(rt_atlas_st: mut RtAtlasState, dir: pointer) -> void {
let names = Fs.list(dir)
if names == null { return }
var i = 0
while i < len(names) {
let file = names[i]
var dot = len(file) - 1
while (dot > 0) and (file[dot] != '.') { dot -= 1 }
if dot > 0 { assets_enqueue(rt_atlas_st, file[0..dot], dir + ("/") + file) }
i += 1
}
}
# ---- incremental preload (#82) --------------------------------------------
# Enqueue a named image file to load later (does not load it now).
function assets_enqueue(rt_atlas_st: mut RtAtlasState, name: pointer, path: pointer) -> void {
atlas_init(rt_atlas_st)
if rt_atlas_st.at_q_n >= ATLAS_MAX_QUEUE { return }
rt_atlas_st.at_q_name[rt_atlas_st.at_q_n] = name
rt_atlas_st.at_q_path[rt_atlas_st.at_q_n] = path
rt_atlas_st.at_q_n += 1
}
# Load up to `max` queued assets this frame, registering each under its name, and
# return how many were loaded by this call. Call it each frame in a loading scene
# (a small `max` keeps the frame short); Assets.ready() flips true when done.
event AssetsReady { } # fired once, the frame the queue finishes
function assets_pump(rt_atlas_st: mut RtAtlasState, rt_audio_st: mut RtAudioState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_truetype_st: mut RtTruetypeState, max: int) -> int {
atlas_init(rt_atlas_st)
var done = 0
while (done < max) and (rt_atlas_st.at_q_pos < rt_atlas_st.at_q_n) {
assets_load_one(rt_atlas_st, rt_audio_st, rt_image_st, rt_inflate_st, rt_truetype_st, rt_atlas_st.at_q_name[rt_atlas_st.at_q_pos], rt_atlas_st.at_q_path[rt_atlas_st.at_q_pos])
rt_atlas_st.at_q_pos += 1
done += 1
}
if (rt_atlas_st.at_q_pos >= rt_atlas_st.at_q_n) and (not rt_atlas_st.at_q_announced) { rt_atlas_st.at_q_announced = true; emit AssetsReady() }
return done
}
# the default loading bar of a `scene X loads then Y`: centred, a fifth of the screen wide
function assets_draw_progress(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState) -> void {
let w = rt_fbw / 2
let x = rt_fbw / 4
let y = rt_fbh / 2 - 4
rt_fill_rect(rt_core_st, x, y, w, 8, 0x303030)
rt_fill_rect(rt_core_st, x, y, w * assets_progress(rt_atlas_st) / 100, 8, 0xffcc44)
}
# what kind of asset a path is, by its extension: sounds and fonts go to their own
# managers (Audio.play(name:), Assets.font(name)); everything else is an image
function path_has_suffix(path: pointer, suffix: pointer) -> bool {
let n = len(path)
let m = len(suffix)
if m > n { return false }
var i = 0
while i < m {
if path[n - m + i] != suffix[i] { return false }
i += 1
}
return true
}
const ATLAS_MAX_FONT: int = 16
function assets_load_one(rt_atlas_st: mut RtAtlasState, rt_audio_st: mut RtAudioState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_truetype_st: mut RtTruetypeState, name: pointer, path: pointer) -> void {
if path_has_suffix(path, ".wav") or path_has_suffix(path, ".mp3") or path_has_suffix(path, ".ogg") {
audio_define(rt_audio_st, name, path)
return
}
if path_has_suffix(path, ".ttf") or path_has_suffix(path, ".ttc") {
if rt_atlas_st.at_font_name == null { rt_atlas_st.at_font_name = pointers(ATLAS_MAX_FONT); rt_atlas_st.at_font_id = words(ATLAS_MAX_FONT) }
if rt_atlas_st.at_font_n < ATLAS_MAX_FONT {
rt_atlas_st.at_font_name[rt_atlas_st.at_font_n] = name
rt_atlas_st.at_font_id[rt_atlas_st.at_font_n] = rt_font_load(rt_image_st, rt_truetype_st, path)
rt_atlas_st.at_font_n += 1
}
return
}
atlas_register_name(rt_atlas_st, name, atlas_image(rt_atlas_st, rt_image_st, rt_inflate_st, path))
}
# the font handle registered under `name` (Assets.font), or 0
function assets_font(rt_atlas_st: RtAtlasState, name: pointer) -> int {
var i = 0
while i < rt_atlas_st.at_font_n {
if rt_atlas_st.at_font_name[i] == name { return rt_atlas_st.at_font_id[i] }
i += 1
}
return 0
}
function assets_total(rt_atlas_st: mut RtAtlasState) -> int { atlas_init(rt_atlas_st); return rt_atlas_st.at_q_n }
function assets_loaded(rt_atlas_st: mut RtAtlasState) -> int { atlas_init(rt_atlas_st); return rt_atlas_st.at_q_pos }
function assets_ready(rt_atlas_st: mut RtAtlasState) -> bool { atlas_init(rt_atlas_st); return rt_atlas_st.at_q_pos >= rt_atlas_st.at_q_n }
# loading progress as a whole-number percent (0..100); an empty queue is 100.
function assets_progress(rt_atlas_st: mut RtAtlasState) -> int {
atlas_init(rt_atlas_st)
if rt_atlas_st.at_q_n <= 0 { return 100 }
return rt_atlas_st.at_q_pos * 100 / rt_atlas_st.at_q_n
}