var graphics = this graphics[prosperon.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] var renderer_id = arg[1] var io = use('io') var os = use('os') var res = use('resources') var json = use('json') var GPU = Symbol() var CPU = Symbol() var LASTUSE = Symbol() var LOADING = Symbol() var cache = new Map() // Image constructor function graphics.Image = function(surfaceData) { // Initialize private properties this[CPU] = surfaceData || undefined; this[GPU] = undefined; this[LOADING] = false; this[LASTUSE] = os.now(); this.rect = {x:0, y:0, width:1, height:1}; } // Define getters and methods on the prototype Object.defineProperties(graphics.Image.prototype, { gpu: { get: function() { this[LASTUSE] = os.now(); if (!this[GPU] && !this[LOADING]) { this[LOADING] = true; var self = this; // Send message to load texture log.console("LOADING") send(renderer_actor, { kind: "renderer", id: renderer_id, op: "loadTexture", data: this[CPU] }, function(response) { log.console("GOT MSG") 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() { this[LASTUSE] = os.now(); // Note: Reading texture back from GPU requires async operation // For now, return the CPU data if available return this[CPU] } }, surface: { get: function() { return this.cpu } }, width: { get: function() { return this[CPU].width } }, height: { get: function() { return this[CPU].height } } }); // Add methods to prototype graphics.Image.prototype.unload_gpu = function() { this[GPU] = undefined } graphics.Image.prototype.unload_cpu = function() { this[CPU] = undefined } function calc_image_size(img) { if (!img.texture || !img.rect) return return [img.texture.width * img.rect.width, img.texture.height * img.rect.height] } function decorate_rect_px(img) { // needs a GPU texture to measure if (!img || !img.texture) return // 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 // store pixel-space version: [x, y, w, h] in texels img.rect_px = { x:Math.round(img.rect.x * img.texture.width), y:Math.round(img.rect.y * img.texture.height), width:Math.round(img.rect.width * img.texture.width), height:Math.round(img.rect.height * img.texture.height) } } function make_handle(obj) { return new graphics.Image(obj); } function wrapSurface(surf, maybeRect){ const 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': return graphics.make_gif(bytes) case 'ase': case 'aseprite': return graphics.make_aseprite(bytes) default: return {surface:graphics.make_texture(bytes)} } } function create_image(path){ try{ const 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}, … } ── */ const anims = {}; for(const [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() { return [this.texture.width, this.texture.height].scale([this.rect[2], this.rect[3]]) } image.dimensions[prosperon.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[prosperon.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 undefined var image = graphics.make_texture(data); var img = make_handle(image) 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.has(id)) return cache.get(id) var ipath = res.find_image(id) if (!ipath) throw new Error(`unknown image ${id}`) var image = create_image(ipath) cache.set(id, image) return image } graphics.texture[prosperon.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, it’s returned directly. ` graphics.texture.cache = {} graphics.texture.time_cache = {} 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[prosperon.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[prosperon.DOC] = ` :return: The total estimated GPU memory usage of all cached textures, in bytes. (Not yet implemented.) ` graphics.tex_hotreload = function tex_hotreload(file) { log.console(`hot reloading ${file}`) if (!(file in graphics.texture.cache)) return log.console('really doing it') var img = create_image(file) var oldimg = graphics.texture.cache[file] log.console(`new image:${json.encode(img)}`) log.console(`old image: ${json.encode(oldimg)}`) merge_objects(oldimg, img, ['surface', 'texture', 'loop', 'time']) } graphics.tex_hotreload[prosperon.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) send(renderer_actor, { kind: "renderer", id: renderer_id, 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.get_font[prosperon.DOC] = ` :param path: A string path to a font file, optionally with ".size" appended. :param size: Pixel size of the font, if not included in 'path'. :return: A font object with .surface and .texture for rendering text. Load a font from file if not cached, or retrieve from cache if already loaded. ` 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] } graphics.queue_sprite_mesh[prosperon.DOC] = ` :param queue: An array of draw commands, some of which are {type:'sprite'} objects. :return: An array of references to GPU buffers [pos,uv,color,indices]. Builds a single geometry mesh for all sprite-type commands in the queue, storing first_index/num_indices so they can be rendered in one draw call. ` graphics.make_text_buffer[prosperon.DOC] = ` :param text: The string to render. :param rect: A rectangle specifying position and possibly wrapping. :param angle: Rotation angle (unused or optional). :param color: A color for the text (could be a vec4). :param wrap: The width in pixels to wrap text, or 0 for no wrap. :param font: A font object created by graphics.make_font or graphics.get_font. :return: A geometry buffer mesh (pos, uv, color, indices) for rendering text. Generate a GPU buffer mesh of text quads for rendering with a font, etc. ` graphics.rectpack[prosperon.DOC] = ` :param width: The width of the area to pack into. :param height: The height of the area to pack into. :param sizes: An array of [w,h] pairs for the rectangles to pack. :return: An array of [x,y] coordinates placing each rect, or null if they don't fit. Perform a rectangle packing using the stbrp library. Return positions for each rect. ` graphics.make_texture[prosperon.DOC] = ` :param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer. :return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering. Convert raw image bytes into an SDL_Surface object. ` graphics.make_gif[prosperon.DOC] = ` :param data: An ArrayBuffer containing GIF data. :return: An object with frames[], each frame having its own .surface. Some also have a .texture for GPU use. Load a GIF, returning its frames. If it's a single-frame GIF, the result may have .surface only. ` graphics.make_aseprite[prosperon.DOC] = ` :param data: An ArrayBuffer containing Aseprite (ASE) file data. :return: An object containing frames or animations, each with .surface. May also have top-level .surface for a single-layer case. Load an Aseprite/ASE file from an array of bytes, returning frames or animations. ` graphics.cull_sprites[prosperon.DOC] = ` :param sprites: An array of sprite objects (each has rect or transform). :param camera: A camera or bounding rectangle defining the view area. :return: A new array of sprites that are visible in the camera's view. Filter an array of sprites to only those visible in the provided camera’s view. ` graphics.make_font[prosperon.DOC] = ` :param data: TTF/OTF file data as an ArrayBuffer. :param size: Pixel size for rendering glyphs. :return: A font object with surface, texture, and glyph data, for text rendering with make_text_buffer. Load a font from TTF/OTF data at the given size. ` graphics.make_line_prim[prosperon.DOC] = ` :param points: An array of [x,y] points forming the line. :param thickness: The thickness (width) of the polyline. :param startCap: (Unused) Possibly the type of cap for the start. :param endCap: (Unused) Possibly the type of cap for the end. :param color: A color to apply to the line. :return: A geometry mesh object suitable for rendering the line via a pipeline command. Build a GPU mesh representing a thick polyline from an array of points, using parsl or a similar library under the hood. ` return graphics