Files
cell/scripts/modules/graphics.js
John Alanbrook 1040c61863
Some checks failed
Build and Deploy / build-macos (push) Failing after 8s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
fix blob usage errors
2025-05-28 23:56:18 -05:00

456 lines
14 KiB
JavaScript
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[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
console.log("LOADING")
send(renderer_actor, {
kind: "renderer",
id: renderer_id,
op: "loadTexture",
data: this[CPU]
}, function(response) {
console.log("GOT MSG")
if (response.error) {
console.error("Failed to load texture:")
console.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){
console.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, its 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) {
console.log(`hot reloading ${file}`)
if (!(file in graphics.texture.cache)) return
console.log('really doing it')
var img = create_image(file)
var oldimg = graphics.texture.cache[file]
console.log(`new image:${json.encode(img)}`)
console.log(`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) {
console.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 cameras 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