animations

This commit is contained in:
2025-07-15 15:06:06 -05:00
5 changed files with 294 additions and 155 deletions

View File

@@ -19,94 +19,97 @@ var LASTUSE = Symbol()
var LOADING = Symbol()
var cache = {}
var pending_gpu_loads = []
graphics.setup = function(renderer)
{
renderer_actor = renderer
// Process any pending GPU loads
if (renderer_actor && pending_gpu_loads.length > 0) {
log.console(`Processing ${pending_gpu_loads.length} pending GPU loads`)
for (var img of pending_gpu_loads) {
img.loadGPU()
}
pending_gpu_loads = []
}
// Also process any cached images that need GPU loading
if (renderer_actor) {
for (var key in cache) {
var img = cache[key]
if (img instanceof graphics.Image && img.cpu && img.gpu == 0) {
img.loadGPU()
}
}
}
}
// Image constructor function
graphics.Image = function(surfaceData) {
// Initialize private properties
this[CPU] = surfaceData || null;
this[GPU] = null;
// Initialize properties
this.cpu = surfaceData || null;
this.gpu = 0;
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();
this.rect = {x:0, y:0, width:surfaceData.width, height:surfaceData.height};
// Load GPU texture if renderer is available, otherwise queue it
if (renderer_actor && this.cpu) {
this.loadGPU();
} else if (this.cpu) {
// Queue for later GPU loading when renderer is available
pending_gpu_loads.push(this)
}
}
// 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;
}
});
graphics.Image.prototype.loadGPU = function() {
if (!this[LOADING] && renderer_actor && this.cpu) {
this[LOADING] = true;
var self = this;
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 {
// Store the full response as texture (has width/height)
self.texture = response;
// Store just the ID as gpu
self.gpu = response.id || 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
this.gpu = 0;
this.texture = 0;
}
graphics.Image.prototype.unload_cpu = function() {
this[CPU] = null
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]
} else if (img.cpu) {
return [img.cpu.width * img.rect.width, img.cpu.height * img.rect.height]
}
return [0, 0]
}
@@ -119,9 +122,9 @@ function decorate_rect_px(img) {
if (img.texture) {
width = img.texture.width;
height = img.texture.height;
} else if (img[CPU]) {
width = img[CPU].width;
height = img[CPU].height;
} else if (img.cpu) {
width = img.cpu.width;
height = img.cpu.height;
} else {
return;
}
@@ -148,11 +151,15 @@ function wrapSurface(surf, maybeRect){
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 */
}));
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 }
@@ -161,13 +168,10 @@ function makeAnim(frames, loop=true){
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 'gif': return graphics.make_gif(bytes) // returns array of surfaces
case 'ase':
case 'aseprite': return graphics.make_aseprite(bytes)
default: return {surface:graphics.make_texture(bytes)}
default: return graphics.make_texture(bytes) // returns single surface
}
}
@@ -175,32 +179,47 @@ function create_image(path){
try{
def bytes = io.slurpbytes(path);
let raw = decode_image(bytes, path.ext());
let raw = decode_image(bytes, path.ext());
/* ── Case A: static image ─────────────────────────────────── */
if(raw.surface) {
var gg = new graphics.Image(raw.surface)
return gg
/* ── Case A: single surface (from make_texture) ────────────── */
if(raw && raw.width && raw.pixels && !Array.isArray(raw)) {
return new graphics.Image(raw)
}
/* ── 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 );
/* ── 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);
}
/* ── Case C: ASE helpers returned { animName:{frames,loop}, … } or single frame ── */
if(typeof raw == 'object' && !raw.width) {
// Check if it's a single surface from ASE (single frame, no tags)
if(raw.surface) {
return new graphics.Image(raw.surface)
}
// Check if it's an untagged animation (multiple frames, no tags)
// This happens when ASE has no tags but multiple frames
if(raw.frames && Array.isArray(raw.frames) && raw.loop != null) {
log.console(`[Graphics] Loading untagged ASE animation with ${raw.frames.length} frames`)
return makeAnim(wrapFrames(raw.frames), !!raw.loop);
}
// Multiple named animations from ASE (with tags)
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;
}
if(Object.keys(anims).length) return anims;
throw new Error('Unsupported image structure from decoder');
@@ -216,9 +235,9 @@ image.dimensions = function() {
if (this.texture) {
width = this.texture.width;
height = this.texture.height;
} else if (this[CPU]) {
width = this[CPU].width;
height = this[CPU].height;
} else if (this.cpu) {
width = this.cpu.width;
height = this.cpu.height;
}
return [width, height].scale([this.rect[2], this.rect[3]])
}
@@ -287,9 +306,9 @@ graphics.texture = function texture(path) {
if (!ipath)
throw new Error(`unknown image ${id}`)
var image = create_image(ipath)
cache[id] = image
return image
var result = create_image(ipath)
cache[id] = result
return result // Can be Image, animation, or collection of animations
}
graphics.texture[cell.DOC] = `
:param path: A string path to an image file or an already-loaded image object.
@@ -330,24 +349,28 @@ graphics.tex_hotreload = function tex_hotreload(file) {
var oldimg = cache[basename]
// Preserve the GPU texture ID if it exists
var oldGPU = oldimg[GPU]
var oldGPU = oldimg.gpu
// Update the CPU surface data
oldimg[CPU] = img[CPU]
oldimg.cpu = img.cpu
oldimg.surface = img.cpu
// Clear GPU texture to force reload
oldimg[GPU] = null
oldimg.gpu = 0
oldimg.texture = 0
oldimg[LOADING] = false
// Update dimensions
if (img[CPU]) {
oldimg.rect = {x:0, y:0, width:img[CPU].width, height:img[CPU].height}
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)
}
// If the texture was on GPU, trigger reload
if (oldGPU && renderer_actor) {
oldimg.gpu // This getter will trigger the reload
oldimg.loadGPU()
}
}
graphics.tex_hotreload[cell.DOC] = `