Files
cell/scripts/graphics.cm
2025-10-24 11:06:37 -05:00

402 lines
10 KiB
Plaintext

var graphics = this
var io = use('io')
var time = use('time')
var res = use('resources')
var json = use('json')
var os = use('os')
var staef = use('staef')
var qoi = use('qoi')
var LASTUSE = Symbol()
var LOADING = Symbol()
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();
}
graphics.Image.prototype.unload_cpu = function() {
this.cpu = null;
this.surface = null;
}
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:Math.round(img.rect.x * width),
y:Math.round(img.rect.y * height),
width:Math.round(img.rect.width * width),
height:Math.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 arr.map(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 graphics.make_gif(bytes) // returns array of surfaces
case 'ase':
case 'aseprite': return graphics.make_aseprite(bytes)
case 'qoi': return qoi.decode(bytes) // returns single surface
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 make_texture for other formats
return graphics.image_decode(bytes) // returns single surface
}
}
function create_image(path){
try{
def bytes = io.slurpbytes(path);
let raw = decode_image(bytes, path.ext());
/* ── Case A: single surface (from make_texture) ────────────── */
if(raw && raw.width && raw.pixels && !Array.isArray(raw)) {
return new graphics.Image(raw)
}
/* ── Case B: array of surfaces (from make_gif) ────────────── */
if(Array.isArray(raw)) {
// Single frame GIF returns array with one surface
if(raw.length == 1 && !raw[0].time) {
return new graphics.Image(raw[0])
}
// Multiple frames - create animation
return makeAnim(wrapFrames(raw), true);
}
if(typeof raw == 'object' && !raw.width) {
if(raw.surface)
return new graphics.Image(raw.surface)
if(raw.frames && Array.isArray(raw.frames) && raw.loop != null)
return makeAnim(wrapFrames(raw.frames), !!raw.loop);
def anims = {};
for(def [name, anim] of Object.entries(raw)){
if(anim && Array.isArray(anim.frames))
anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop);
else if(anim && anim.surface)
anims[name] = new graphics.Image(anim.surface);
}
if(Object.keys(anims).length) return anims;
}
throw new 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 (!(data instanceof ArrayBuffer)) 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 (typeof id != 'string')
throw new Error('Expected a string ID')
if (data instanceof ArrayBuffer)
return graphics.texture_from_data(data)
}
graphics.texture = function texture(path) {
if (path instanceof graphics.Image) return path
if (typeof path != 'string')
throw new Error('need a string for graphics.texture')
var parts = path.split(':')
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 && !isNaN(parseInt(animName))) {
frameIndex = parseInt(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 && Array.isArray(cached.frames)) {
var idx = parseInt(frameIndex)
if (isNaN(idx)) 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 (cached instanceof graphics.Image) {
return cached
}
}
// If cached is a single Image, treat it as a single-frame animation
if (cached instanceof 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 && Array.isArray(cached.frames)) {
if (frameIndex != null) {
var idx = parseInt(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 (typeof cached == 'object' && !cached.frames) {
var anim = cached[animName]
if (!anim)
throw new Error(`animation ${animName} not found in ${id}`)
if (frameIndex != null) {
var idx = parseInt(frameIndex)
if (isNaN(idx)) return anim
if (anim instanceof graphics.Image) {
// Single image animation - any frame index returns the image
return anim
} else if (anim.frames && Array.isArray(anim.frames)) {
// Multi-frame animation - wrap the index
idx = idx % anim.frames.length
return anim.frames[idx].image
}
}
// Just animation name - return the animation
return anim
}
return cached
}
graphics.tex_hotreload = function tex_hotreload(file) {
var basename = file.split('/').pop().split('.')[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) {
arr.forEach(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 (typeof path != 'string') return path
var parts = path.split('.')
var size = 16 // default size
if (!isNaN(parts[1])) {
path = parts[0]
size = Number(parts[1])
}
var fullpath = res.find_font(path)
if (!fullpath) throw new Error(`Cannot load font ${path}`)
var fontstr = `${fullpath}.${size}`
if (fontcache[fontstr]) return fontcache[fontstr]
var data = io.slurpbytes(fullpath)
var font = new staef.font(data, size)
fontcache[fontstr] = font
return font
}
graphics.queue_sprite_mesh = function(queue) {
var sprites = queue.filter(x => x.type == 'sprite')
if (sprites.length == 0) return []
var mesh = graphics.make_sprite_mesh(sprites)
for (var i = 0; i < sprites.length; 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