Files
cell/scripts/graphics.cm
2025-07-14 09:52:25 -05:00

427 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var graphics = this
graphics[cell.DOC] = `
Provides functionality for loading and managing images, fonts, textures, and sprite meshes.
Includes both JavaScript and C-implemented routines for creating geometry buffers, performing
rectangle packing, etc.
`
var renderer_actor = arg?.[0] || null
var io = use('io')
var time = use('time')
var res = use('resources')
var json = use('json')
var GPU = Symbol()
var CPU = Symbol()
var LASTUSE = Symbol()
var LOADING = Symbol()
var cache = {}
graphics.setup = function(renderer)
{
renderer_actor = renderer
}
// Image constructor function
graphics.Image = function(surfaceData) {
// Initialize private properties
this[CPU] = surfaceData || null;
this[GPU] = null;
this[LOADING] = false;
this[LASTUSE] = time.number();
this.rect = {x:0, y:0, width:surfaceData.width, height:surfaceData.height};
}
// Define getters and methods on the prototype
Object.defineProperties(graphics.Image.prototype, {
gpu: {
get: function() {
if (!this[GPU] && !this[LOADING] && renderer_actor) {
this[LOADING] = true;
var self = this;
// Send message to load texture
send(renderer_actor, {
kind: "renderer",
op: "loadTexture",
data: this[CPU]
}, function(response) {
if (response.error) {
log.error("Failed to load texture:")
log.error(response.error)
self[LOADING] = false;
} else {
self[GPU] = response;
decorate_rect_px(self);
self[LOADING] = false;
}
});
}
return this[GPU]
}
},
texture: {
get: function() { return this.gpu }
},
cpu: {
get: function() {
return this[CPU]
}
},
surface: {
get: function() { return this.cpu }
},
width: {
get: function() {
return this[CPU]?.width || 0
}
},
height: {
get: function() {
return this[CPU]?.height || 0
}
}
});
// Add methods to prototype
graphics.Image.prototype.unload_gpu = function() {
this[GPU] = null
}
graphics.Image.prototype.unload_cpu = function() {
this[CPU] = 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 => ({
image : wrapSurface(f.surface || f), /* accept bare surface too */
time: f.time,
rect: f.rect /* keep for reference */
}));
}
function makeAnim(frames, loop=true){
return { frames, loop }
}
function decode_image(bytes, ext)
{
switch(ext) {
case 'gif':
var g = graphics.make_gif(bytes)
if (g.frames) return g.frames[0]
return g
case 'ase':
case 'aseprite': return graphics.make_aseprite(bytes)
default: return {surface:graphics.make_texture(bytes)}
}
}
function create_image(path){
try{
def bytes = io.slurpbytes(path);
let raw = decode_image(bytes, path.ext());
/* ── Case A: static image ─────────────────────────────────── */
if(raw.surface) {
var gg = new graphics.Image(raw.surface)
return gg
}
/* ── Case B: GIF helpers returned array [surf, …] ─────────── */
if(Array.isArray(raw))
return makeAnim( wrapFrames(raw), true );
/* ── Case C: GIF helpers returned {frames,loop} ───────────── */
if(raw.frames && Array.isArray(raw.frames))
return makeAnim( wrapFrames(raw.frames), !!raw.loop );
/* ── Case D: ASE helpers returned { animName:{frames,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) /* ase with flat surface */
anims[name] = makeAnim(
[{image:make_handle(anim.surface),time:0}], true );
}
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]])
}
image.dimensions[cell.DOC] = `
:return: A 2D array [width, height] that is the scaled size of this image (texture size * rect size).
`
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.is_image[cell.DOC] = `
:param obj: An object to check.
:return: True if 'obj' has a .texture and a .rect property, indicating it's an image object.
`
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(id, 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 id = path //.split(':')[0]
if (cache[id]) return cache[id]
var ipath = res.find_image(id)
if (!ipath)
throw new Error(`unknown image ${id}`)
var image = create_image(ipath)
cache[id] = image
return image
}
graphics.texture[cell.DOC] = `
:param path: A string path to an image file or an already-loaded image object.
:return: An image object with {surface, texture, frames?, etc.} depending on the format.
Load or retrieve a cached image, converting it into a GPU texture. If 'path' is already an object, its returned directly.
`
graphics.texture.total_size = function() {
var size = 0
// Not yet implemented, presumably sum of (texture.width * texture.height * 4) for images in RAM
return size
}
graphics.texture.total_size[cell.DOC] = `
:return: The total estimated memory size of all cached textures in RAM, in bytes. (Not yet implemented.)
`
graphics.texture.total_vram = function() {
var vram = 0
// Not yet implemented, presumably sum of GPU memory usage
return vram
}
graphics.texture.total_vram[cell.DOC] = `
:return: The total estimated GPU memory usage of all cached textures, in bytes. (Not yet implemented.)
`
graphics.tex_hotreload = function tex_hotreload(file) {
// Extract just the filename without path and extension
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]
// Clear GPU texture to force reload
oldimg[GPU] = null
oldimg[LOADING] = false
// Update dimensions
if (img[CPU]) {
oldimg.rect = {x:0, y:0, width:img[CPU].width, height:img[CPU].height}
decorate_rect_px(oldimg)
}
// If the texture was on GPU, trigger reload
if (oldGPU && renderer_actor) {
oldimg.gpu // This getter will trigger the reload
}
}
graphics.tex_hotreload[cell.DOC] = `
:param file: The file path that was changed on disk.
:return: None
Reload the image for the given file, updating the cached copy in memory and GPU.
`
/**
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, size) {
var parts = path.split('.')
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 = graphics.make_font(data,size)
// Load font texture via renderer actor (async)
if (renderer_actor) {
send(renderer_actor, {
kind: "renderer",
op: "loadTexture",
data: font.surface
}, function(response) {
if (response.error) {
log.error("Failed to load font texture:", response.error);
} else {
font.texture = response;
}
});
}
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