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(); } 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:number.round(img.rect.x * width), y:number.round(img.rect.y * height), width:number.round(img.rect.width * width), height:number.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 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 decoded.frames.map(function(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: decoded.frames.map(function(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 = path.split('.').pop() var raw = decode_image(bytes, ext); /* ── Case A: single surface (from make_texture) ────────────── */ if(raw && raw.width && raw.pixels && !isa(raw, array)) { return new graphics.Image(raw) } /* ── Case B: array of surfaces (from make_gif) ────────────── */ if(isa(raw, array)) { // 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 && isa(raw.frames, array) && raw.loop != null) return makeAnim(wrapFrames(raw.frames), !!raw.loop); def anims = {}; for(def [name, anim] of Object.entries(raw)){ if(anim && isa(anim.frames, array)) anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop); else if(anim && anim.surface) anims[name] = new graphics.Image(anim.surface); } if(array(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 && !isa(number(animName), null)) { 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 && isa(cached.frames, array)) { 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 (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 && isa(cached.frames, array)) { 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 (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 = number(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 && isa(anim.frames, array)) { // 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 (isa(path, object)) return path if (!isa(path, text)) throw new Error(`Can't find font with path: ${path}`) var parts = path.split('.') 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 new 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 = 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