451 lines
12 KiB
Plaintext
451 lines
12 KiB
Plaintext
var graphics = {}
|
|
|
|
var io = use('cellfs')
|
|
var time = use('time')
|
|
var res = use('resources')
|
|
var json = use('json')
|
|
var os = use('os')
|
|
var staef = use('staef')
|
|
var qoi = use('image/qoi')
|
|
var png = use('image/png')
|
|
var gif = use('image/gif')
|
|
var aseprite = use('image/aseprite')
|
|
|
|
var LASTUSE = "graphics:lastuse"
|
|
var LOADING = "graphics:loading"
|
|
|
|
var cache = {}
|
|
|
|
// cpu is the surface
|
|
graphics.Image = function(surfaceData) {
|
|
this.cpu = surfaceData || null;
|
|
this.texture = 0;
|
|
this.surface = this.cpu;
|
|
this.width = surfaceData?.width || 0;
|
|
this.height = surfaceData?.height || 0;
|
|
this.rect = {x:0, y:0, width:this.width, height:this.height};
|
|
this[LOADING] = false;
|
|
this[LASTUSE] = time.number();
|
|
}
|
|
|
|
function calc_image_size(img) {
|
|
if (!img.rect) return
|
|
if (img.texture) {
|
|
return [img.texture.width * img.rect.width, img.texture.height * img.rect.height]
|
|
} else if (img.cpu) {
|
|
return [img.cpu.width * img.rect.width, img.cpu.height * img.rect.height]
|
|
}
|
|
return [0, 0]
|
|
}
|
|
|
|
function decorate_rect_px(img) {
|
|
// default UV rect is the whole image if none supplied
|
|
img.rect ??= {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1
|
|
|
|
var width = 0, height = 0;
|
|
if (img.texture) {
|
|
width = img.texture.width;
|
|
height = img.texture.height;
|
|
} else if (img.cpu) {
|
|
width = img.cpu.width;
|
|
height = img.cpu.height;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// store pixel-space version: [x, y, w, h] in texels
|
|
img.rect_px = {
|
|
x:round(img.rect.x * width),
|
|
y:round(img.rect.y * height),
|
|
width:round(img.rect.width * width),
|
|
height:round(img.rect.height * height)
|
|
}
|
|
}
|
|
|
|
function make_handle(obj)
|
|
{
|
|
var img = new graphics.Image(obj);
|
|
decorate_rect_px(img);
|
|
return img;
|
|
}
|
|
|
|
function wrapSurface(surf, maybeRect){
|
|
def h = make_handle(surf);
|
|
if(maybeRect) h.rect = maybeRect; /* honour frame sub-rect */
|
|
return h;
|
|
}
|
|
function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */
|
|
return array(arr, f => {
|
|
// Handle both surface objects and objects with surface property
|
|
var surf = f.surface || f;
|
|
return {
|
|
image: wrapSurface(surf),
|
|
time: f.time || 0,
|
|
rect: f.rect /* keep for reference */
|
|
}
|
|
});
|
|
}
|
|
function makeAnim(frames, loop=true){
|
|
return { frames, loop }
|
|
}
|
|
|
|
function decode_image(bytes, ext)
|
|
{
|
|
switch(ext) {
|
|
case 'gif': return decode_gif(gif.decode(bytes))
|
|
case 'ase':
|
|
case 'aseprite': return decode_aseprite(aseprite.decode(bytes))
|
|
case 'qoi': return qoi.decode(bytes) // returns single surface
|
|
case 'png': return png.decode(bytes) // returns single surface
|
|
case 'jpg':
|
|
case 'jpeg': return png.decode(bytes) // png.decode handles jpg too via stb_image
|
|
case 'bmp': return png.decode(bytes) // png.decode handles bmp too via stb_image
|
|
default:
|
|
// Try QOI first since it's fast to check
|
|
var qoi_result = qoi.decode(bytes)
|
|
if (qoi_result) return qoi_result
|
|
// Fall back to png decoder for other formats (uses stb_image)
|
|
return png.decode(bytes)
|
|
}
|
|
}
|
|
|
|
// Convert gif.decode output to graphics.cm format
|
|
function decode_gif(decoded) {
|
|
if (!decoded || !decoded.frames) return null
|
|
|
|
// Single frame - return just the surface
|
|
if (decoded.frame_count == 1) {
|
|
return decoded.frames[0]
|
|
}
|
|
|
|
// Multiple frames - return array with time property
|
|
return array(decoded.frames, frame => {
|
|
return {
|
|
surface: frame,
|
|
time: (frame.duration || 100) / 1000.0 // convert ms to seconds
|
|
}
|
|
})
|
|
}
|
|
|
|
// Convert aseprite.decode output to graphics.cm format
|
|
function decode_aseprite(decoded) {
|
|
if (!decoded) return null
|
|
|
|
// Single frame - return just the surface
|
|
if (decoded.frame_count == 1) {
|
|
return { surface: decoded.frames[0] }
|
|
}
|
|
|
|
// Multiple frames without tags - return as single animation
|
|
return {
|
|
frames: array(decoded.frames, frame => {
|
|
return {
|
|
surface: frame,
|
|
time: (frame.duration || 100) / 1000.0 // convert ms to seconds
|
|
}
|
|
}),
|
|
loop: true
|
|
}
|
|
}
|
|
|
|
function create_image(path){
|
|
try{
|
|
def bytes = io.slurp(path);
|
|
|
|
var ext = array(path, '.').pop()
|
|
var raw = decode_image(bytes, ext);
|
|
|
|
/* ── Case A: single surface (from make_texture) ────────────── */
|
|
if(raw && raw.width && raw.pixels && !is_array(raw)) {
|
|
return new graphics.Image(raw)
|
|
}
|
|
|
|
/* ── Case B: array of surfaces (from make_gif) ────────────── */
|
|
if(is_array(raw)) {
|
|
// Single frame GIF returns array with one surface
|
|
if(length(raw) == 1 && !raw[0].time) {
|
|
return new graphics.Image(raw[0])
|
|
}
|
|
// Multiple frames - create animation
|
|
return makeAnim(wrapFrames(raw), true);
|
|
}
|
|
|
|
if(is_object(raw) && !raw.width) {
|
|
if(raw.surface)
|
|
return new graphics.Image(raw.surface)
|
|
|
|
if(raw.frames && is_array(raw.frames) && raw.loop != null)
|
|
return makeAnim(wrapFrames(raw.frames), !!raw.loop);
|
|
|
|
def anims = {};
|
|
var keys = array(raw)
|
|
arrfor(keys, function(name) {
|
|
var anim = raw[name]
|
|
if(anim && is_array(anim.frames))
|
|
anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop);
|
|
else if(anim && anim.surface)
|
|
anims[name] = new graphics.Image(anim.surface);
|
|
})
|
|
if(length(array(anims))) return anims;
|
|
}
|
|
|
|
throw Error('Unsupported image structure from decoder');
|
|
|
|
}catch(e){
|
|
log.error(`Error loading image ${path}: ${e.message}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
var image = {}
|
|
image.dimensions = function() {
|
|
var width = 0, height = 0;
|
|
if (this.texture) {
|
|
width = this.texture.width;
|
|
height = this.texture.height;
|
|
} else if (this.cpu) {
|
|
width = this.cpu.width;
|
|
height = this.cpu.height;
|
|
}
|
|
return [width, height].scale([this.rect[2], this.rect[3]])
|
|
}
|
|
|
|
var spritesheet
|
|
var sheet_frames = []
|
|
var sheetsize = 1024
|
|
|
|
/**
|
|
Pack multiple images into a single texture sheet for efficiency.
|
|
Currently unimplemented (returns immediately).
|
|
*/
|
|
function pack_into_sheet(images) {
|
|
return
|
|
// This code is currently disabled with an immediate return.
|
|
// Implementation details commented out below.
|
|
}
|
|
|
|
graphics.is_image = function(obj) {
|
|
if (obj.texture && obj.rect) return true
|
|
}
|
|
|
|
graphics.texture_from_data = function(data)
|
|
{
|
|
if (!is_blob(data)) return null
|
|
|
|
var image = graphics.make_texture(data);
|
|
var img = make_handle(image)
|
|
|
|
if (renderer_actor) img.gpu;
|
|
|
|
return img;
|
|
}
|
|
|
|
graphics.from_surface = function(surf)
|
|
{
|
|
return make_handle(surf)
|
|
}
|
|
|
|
graphics.from = function(id, data)
|
|
{
|
|
if (!is_text(id))
|
|
throw Error('Expected a string ID')
|
|
|
|
if (is_blob(data))
|
|
return graphics.texture_from_data(data)
|
|
}
|
|
|
|
graphics.texture = function texture(path) {
|
|
if (is_proto(path, graphics.Image)) return path
|
|
|
|
if (!is_text(path))
|
|
throw Error('need a string for graphics.texture')
|
|
|
|
var parts = array(path, ':')
|
|
var id = parts[0]
|
|
var animName = parts[1]
|
|
var frameIndex = parts[2]
|
|
|
|
// Handle the case where animName is actually a frame index (e.g., "gears:0")
|
|
if (animName != null && frameIndex == null && !is_null(number(animName))) {
|
|
frameIndex = number(animName)
|
|
animName = null
|
|
}
|
|
|
|
if (!cache[id]) {
|
|
var ipath = res.find_image(id)
|
|
|
|
if (!ipath) {
|
|
// If still not found, return notex
|
|
return graphics.texture('notex')
|
|
}
|
|
|
|
var result = create_image(ipath)
|
|
cache[id] = result
|
|
}
|
|
|
|
var cached = cache[id]
|
|
|
|
// No further path specifiers and no frame index - return the whole thing
|
|
if (!animName && frameIndex == null) return cached
|
|
|
|
// Handle frame index without animation name (e.g., "gears:0")
|
|
if (!animName && frameIndex != null) {
|
|
// If cached is a single animation (has .frames property)
|
|
if (cached.frames && is_array(cached.frames)) {
|
|
var idx = number(frameIndex)
|
|
if (idx == null) return cached
|
|
// Wrap the index
|
|
idx = idx % cached.frames.length
|
|
return cached.frames[idx].image
|
|
}
|
|
// If cached is a single Image, any frame index just returns the image
|
|
if (is_proto(cached, graphics.Image)) {
|
|
return cached
|
|
}
|
|
}
|
|
|
|
// If cached is a single Image, treat it as a single-frame animation
|
|
if (is_proto(cached, graphics.Image)) {
|
|
if (frameIndex != null) {
|
|
// For single images, any frame index just returns the image
|
|
return cached
|
|
}
|
|
// animName without frameIndex for single image - return as single-frame array
|
|
return [cached]
|
|
}
|
|
|
|
// If cached is a single animation (has .frames property)
|
|
if (cached.frames && is_array(cached.frames)) {
|
|
if (frameIndex != null) {
|
|
var idx = number(frameIndex)
|
|
if (isNaN(idx)) return cached
|
|
// Wrap the index
|
|
idx = idx % cached.frames.length
|
|
return cached.frames[idx].image
|
|
}
|
|
// Just animation name for single animation - return the animation
|
|
return cached
|
|
}
|
|
|
|
// If cached is an object of multiple animations
|
|
if (is_object(cached) && !cached.frames) {
|
|
var anim = cached[animName]
|
|
if (!anim)
|
|
throw Error(`animation ${animName} not found in ${id}`)
|
|
|
|
if (frameIndex != null) {
|
|
var idx = number(frameIndex)
|
|
if (isNaN(idx)) return anim
|
|
|
|
if (is_proto(anim, graphics.Image)) {
|
|
// Single image animation - any frame index returns the image
|
|
return anim
|
|
} else if (anim.frames && is_array(anim.frames)) {
|
|
// Multi-frame animation - wrap the index
|
|
idx = idx % length(anim.frames)
|
|
return anim.frames[idx].image
|
|
}
|
|
}
|
|
|
|
// Just animation name - return the animation
|
|
return anim
|
|
}
|
|
|
|
return cached
|
|
}
|
|
|
|
graphics.tex_hotreload = function tex_hotreload(file) {
|
|
var basename = array(array(file, '/').pop(), '.')[0]
|
|
|
|
// Check if this basename exists in our cache
|
|
if (!(basename in cache)) return
|
|
|
|
// Find the full path for this image
|
|
var fullpath = res.find_image(basename)
|
|
if (!fullpath) return
|
|
|
|
var img = create_image(fullpath)
|
|
var oldimg = cache[basename]
|
|
|
|
// Preserve the GPU texture ID if it exists
|
|
var oldGPU = oldimg.gpu
|
|
|
|
// Update the CPU surface data
|
|
oldimg.cpu = img.cpu
|
|
oldimg.surface = img.cpu
|
|
|
|
// Clear GPU texture to force reload
|
|
oldimg.gpu = 0
|
|
oldimg.texture = 0
|
|
oldimg[LOADING] = false
|
|
|
|
// Update dimensions
|
|
if (img.cpu) {
|
|
oldimg.width = img.cpu.width
|
|
oldimg.height = img.cpu.height
|
|
oldimg.rect = {x:0, y:0, width:img.cpu.width, height:img.cpu.height}
|
|
decorate_rect_px(oldimg)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Merges specific properties from nv into ov, using an array of property names.
|
|
*/
|
|
function merge_objects(ov, nv, arr) {
|
|
arrfor(arr, x => ov[x] = nv[x])
|
|
}
|
|
|
|
/**
|
|
Unimplemented function for creating a spritesheet out of multiple images.
|
|
*/
|
|
function make_spritesheet(paths, width, height) {
|
|
return
|
|
}
|
|
|
|
/**
|
|
Stores previously loaded fonts. Keyed by e.g. "path.ttf.16" -> fontObject.
|
|
*/
|
|
var fontcache = {}
|
|
var datas = []
|
|
|
|
graphics.get_font = function get_font(path) {
|
|
if (is_object(path)) return path
|
|
if (!is_text(path))
|
|
throw Error(`Can't find font with path: ${path}`)
|
|
|
|
var parts = array(path, '.')
|
|
var size = 16 // default size
|
|
parts[1] = number(parts[1])
|
|
if (parts[1]) {
|
|
path = parts[0]
|
|
size = parts[1]
|
|
}
|
|
|
|
var fullpath = res.find_font(path)
|
|
if (!fullpath) throw Error(`Cannot load font ${path}`)
|
|
|
|
var fontstr = `${fullpath}.${size}`
|
|
if (fontcache[fontstr]) return fontcache[fontstr]
|
|
|
|
var data = io.slurp(fullpath)
|
|
var font = new staef.font(data, size)
|
|
|
|
fontcache[fontstr] = font
|
|
|
|
return font
|
|
}
|
|
|
|
graphics.queue_sprite_mesh = function(queue) {
|
|
var sprites = filter(queue, x => x.type == 'sprite')
|
|
if (length(sprites) == 0) return []
|
|
var mesh = graphics.make_sprite_mesh(sprites)
|
|
for (var i = 0; i < length(sprites); i++) {
|
|
sprites[i].mesh = mesh
|
|
sprites[i].first_index = i*6
|
|
sprites[i].num_indices = 6
|
|
}
|
|
return [mesh.pos, mesh.uv, mesh.color, mesh.indices]
|
|
}
|
|
|
|
return graphics
|