add graphics back
This commit is contained in:
401
graphics.cm
Normal file
401
graphics.cm
Normal file
@@ -0,0 +1,401 @@
|
||||
var graphics = this
|
||||
|
||||
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('qoi')
|
||||
|
||||
var LASTUSE = Symbol()
|
||||
var LOADING = Symbol()
|
||||
|
||||
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: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 => {
|
||||
// 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 graphics.make_gif(bytes) // returns array of surfaces
|
||||
case 'ase':
|
||||
case 'aseprite': return graphics.make_aseprite(bytes)
|
||||
case 'qoi': return qoi.decode(bytes) // returns single surface
|
||||
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 make_texture for other formats
|
||||
return graphics.image_decode(bytes) // returns single surface
|
||||
}
|
||||
}
|
||||
|
||||
function create_image(path){
|
||||
try{
|
||||
def bytes = io.slurpbytes(path);
|
||||
|
||||
let raw = decode_image(bytes, path.ext());
|
||||
|
||||
/* ── Case A: single surface (from make_texture) ────────────── */
|
||||
if(raw && raw.width && raw.pixels && !Array.isArray(raw)) {
|
||||
return new graphics.Image(raw)
|
||||
}
|
||||
|
||||
/* ── 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);
|
||||
}
|
||||
|
||||
if(typeof raw == 'object' && !raw.width) {
|
||||
if(raw.surface)
|
||||
return new graphics.Image(raw.surface)
|
||||
|
||||
if(raw.frames && Array.isArray(raw.frames) && raw.loop != null)
|
||||
return makeAnim(wrapFrames(raw.frames), !!raw.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)
|
||||
anims[name] = new graphics.Image(anim.surface);
|
||||
}
|
||||
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]])
|
||||
}
|
||||
|
||||
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 && !isNaN(parseInt(animName))) {
|
||||
frameIndex = parseInt(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 && Array.isArray(cached.frames)) {
|
||||
var idx = parseInt(frameIndex)
|
||||
if (isNaN(idx)) 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 && Array.isArray(cached.frames)) {
|
||||
if (frameIndex != null) {
|
||||
var idx = parseInt(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 = parseInt(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 && Array.isArray(anim.frames)) {
|
||||
// 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 (typeof path != 'string') return path
|
||||
var parts = path.split('.')
|
||||
var size = 16 // default size
|
||||
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 = 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
|
||||
148
mersenne.c
Normal file
148
mersenne.c
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "quickjs.h"
|
||||
#include "cell.h"
|
||||
#include <stdint.h>
|
||||
#include "qjs_macros.h"
|
||||
|
||||
// Random number generation constants for MT19937-64
|
||||
#define STATE_VECTOR_LENGTH 312
|
||||
#define STATE_VECTOR_M 156
|
||||
#define NN STATE_VECTOR_LENGTH
|
||||
#define MM STATE_VECTOR_M
|
||||
#define MATRIX_A 0xB5026F5AA96619E9ULL
|
||||
#define UM 0xFFFFFFFF80000000ULL /* Most significant 33 bits */
|
||||
#define LM 0x7FFFFFFFULL /* Least significant 31 bits */
|
||||
|
||||
typedef struct tagMTRand {
|
||||
uint64_t mt[STATE_VECTOR_LENGTH];
|
||||
int32_t index;
|
||||
} MTRand;
|
||||
|
||||
// Random number generation functions
|
||||
static void m_seedRand(MTRand* rand, uint64_t seed) {
|
||||
rand->mt[0] = seed;
|
||||
for(rand->index = 1; rand->index < NN; rand->index++) {
|
||||
rand->mt[rand->index] = (6364136223846793005ULL * (rand->mt[rand->index-1] ^ (rand->mt[rand->index-1] >> 62)) + rand->index);
|
||||
}
|
||||
}
|
||||
|
||||
static int64_t genRandLong(MTRand* rand) {
|
||||
int i;
|
||||
uint64_t x;
|
||||
static uint64_t mag01[2] = {0ULL, MATRIX_A};
|
||||
|
||||
if (rand->index >= NN) { /* generate NN words at one time */
|
||||
/* if init_genrand64() has not been called, */
|
||||
/* a default initial seed is used */
|
||||
if (rand->index == NN+1)
|
||||
m_seedRand(rand, 5489ULL);
|
||||
|
||||
for (i = 0; i < NN-MM; i++) {
|
||||
x = (rand->mt[i] & UM) | (rand->mt[i+1] & LM);
|
||||
rand->mt[i] = rand->mt[i+MM] ^ (x>>1) ^ mag01[(int)(x&1ULL)];
|
||||
}
|
||||
for (; i < NN-1; i++) {
|
||||
x = (rand->mt[i] & UM) | (rand->mt[i+1] & LM);
|
||||
rand->mt[i] = rand->mt[i+(MM-NN)] ^ (x>>1) ^ mag01[(int)(x&1ULL)];
|
||||
}
|
||||
x = (rand->mt[NN-1] & UM) | (rand->mt[0] & LM);
|
||||
rand->mt[NN-1] = rand->mt[MM-1] ^ (x>>1) ^ mag01[(int)(x&1ULL)];
|
||||
|
||||
rand->index = 0;
|
||||
}
|
||||
|
||||
x = rand->mt[rand->index++];
|
||||
|
||||
x ^= (x >> 29) & 0x5555555555555555ULL;
|
||||
x ^= (x << 17) & 0x71D67FFFEDA60000ULL;
|
||||
x ^= (x << 37) & 0xFFF7EEE000000000ULL;
|
||||
x ^= (x >> 43);
|
||||
|
||||
return (int64_t)(x & 0x000FFFFFFFFFFFFFULL); /* return 52-bit value safe for JS */
|
||||
}
|
||||
|
||||
static double genRand(MTRand* rand) {
|
||||
/* generates a random number on [0,1)-real-interval */
|
||||
return (genRandLong(rand) >> 11) * (1.0/9007199254740992.0);
|
||||
}
|
||||
|
||||
/* JS Class Definition */
|
||||
static JSClassID js_mersenne_class_id;
|
||||
|
||||
static void js_mersenne_finalizer(JSRuntime *rt, JSValue val) {
|
||||
MTRand *mrand = JS_GetOpaque(val, js_mersenne_class_id);
|
||||
js_free_rt(rt, mrand);
|
||||
}
|
||||
|
||||
static JSClassDef js_mersenne_class = {
|
||||
"Mersenne",
|
||||
.finalizer = js_mersenne_finalizer,
|
||||
};
|
||||
|
||||
static MTRand *js2mersenne(JSContext *js, JSValue v) {
|
||||
return JS_GetOpaque(v, js_mersenne_class_id);
|
||||
}
|
||||
|
||||
/* Methods */
|
||||
JSC_CCALL(mersenne_get,
|
||||
MTRand *mrand = js2mersenne(js, self);
|
||||
if (!mrand) return JS_ThrowTypeError(js, "Invalid mersenne context");
|
||||
return JS_NewFloat64(js, genRand(mrand));
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_mersenne_funcs[] = {
|
||||
JS_CFUNC_DEF("get", 0, js_mersenne_get),
|
||||
};
|
||||
|
||||
/* Factory Function */
|
||||
static JSValue js_mersenne_use_call(JSContext *js, JSValueConst func_obj,
|
||||
JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
uint64_t seed;
|
||||
|
||||
if (argc == 0 || JS_IsNull(argv[0])) {
|
||||
// Use OS random
|
||||
extern int randombytes(void *buf, size_t n);
|
||||
randombytes(&seed, 8);
|
||||
} else {
|
||||
if (JS_ToFloat64(js, (double*)&seed, argv[0])) {
|
||||
// Fallback to number if bigint fails or is not provided as bigint
|
||||
double d;
|
||||
if (JS_ToFloat64(js, &d, argv[0])) return JS_EXCEPTION;
|
||||
seed = (uint64_t)d;
|
||||
}
|
||||
}
|
||||
|
||||
MTRand *mrand = js_malloc(js, sizeof(MTRand));
|
||||
if (!mrand) return JS_ThrowOutOfMemory(js);
|
||||
|
||||
m_seedRand(mrand, seed);
|
||||
|
||||
JSValue obj = JS_NewObjectClass(js, js_mersenne_class_id);
|
||||
if (JS_IsException(obj)) {
|
||||
js_free(js, mrand);
|
||||
return obj;
|
||||
}
|
||||
|
||||
JS_SetOpaque(obj, mrand);
|
||||
|
||||
// Store seed as a read-only property
|
||||
JS_DefinePropertyValueStr(js, obj, "seed",
|
||||
JS_NewFloat64(js, seed),
|
||||
JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE // Read-only (no WRITABLE)
|
||||
);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
JSValue js_mersenne_use(JSContext *js)
|
||||
{
|
||||
JS_NewClassID(&js_mersenne_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_mersenne_class_id, &js_mersenne_class);
|
||||
|
||||
JSValue proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, proto, js_mersenne_funcs, sizeof(js_mersenne_funcs)/sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_mersenne_class_id, proto);
|
||||
|
||||
// Return the factory function
|
||||
return JS_NewCFunction2(js, js_mersenne_use_call, "mersenne", 1, JS_CFUNC_generic, 0);
|
||||
}
|
||||
25
qjs_math.c
25
qjs_math.c
@@ -63,8 +63,7 @@ static int gcd(int a, int b) {
|
||||
|
||||
// MATH FUNCTIONS
|
||||
|
||||
|
||||
|
||||
// Rotate a 2D point (or array of length 2) by the given angle (in turns) around an optional pivot.
|
||||
JSC_CCALL(math_rotate,
|
||||
HMM_Vec2 vec = js2vec2(js,argv[0]);
|
||||
double angle = js2angle(js, argv[1]);
|
||||
@@ -82,6 +81,7 @@ JSC_CCALL(math_rotate,
|
||||
return vec22js(js, vec);
|
||||
)
|
||||
|
||||
// Return a normalized copy of the given numeric array. For 2D/3D/4D or arbitrary length.
|
||||
JSC_CCALL(math_norm,
|
||||
int len = JS_ArrayLength(js,argv[0]);
|
||||
|
||||
@@ -100,6 +100,7 @@ JSC_CCALL(math_norm,
|
||||
ret = newarr;
|
||||
)
|
||||
|
||||
// Compute the angle between two vectors (2D/3D/4D).
|
||||
JSC_CCALL(math_angle_between,
|
||||
int len = JS_ArrayLength(js,argv[0]);
|
||||
switch(len) {
|
||||
@@ -110,6 +111,7 @@ JSC_CCALL(math_angle_between,
|
||||
return JS_ThrowReferenceError(js, "Input array must have a length between 2 and 4.");
|
||||
)
|
||||
|
||||
// Linear interpolation between two numbers: lerp(a, b, t).
|
||||
JSC_CCALL(math_lerp,
|
||||
double s = js2number(js,argv[0]);
|
||||
double f = js2number(js,argv[1]);
|
||||
@@ -118,14 +120,17 @@ JSC_CCALL(math_lerp,
|
||||
ret = number2js(js,(f-s)*t+s);
|
||||
)
|
||||
|
||||
// Compute the greatest common divisor of two integers.
|
||||
JSC_CCALL(math_gcd, ret = number2js(js,gcd(js2number(js,argv[0]), js2number(js,argv[1]))); )
|
||||
|
||||
// Compute the least common multiple of two integers.
|
||||
JSC_CCALL(math_lcm,
|
||||
double a = js2number(js,argv[0]);
|
||||
double b = js2number(js,argv[1]);
|
||||
ret = number2js(js,(a*b)/gcd(a,b));
|
||||
)
|
||||
|
||||
// Clamp a number between low and high. clamp(value, low, high).
|
||||
JSC_CCALL(math_clamp,
|
||||
double x = js2number(js,argv[0]);
|
||||
double l = js2number(js,argv[1]);
|
||||
@@ -133,6 +138,7 @@ JSC_CCALL(math_clamp,
|
||||
return number2js(js,x > h ? h : x < l ? l : x);
|
||||
)
|
||||
|
||||
// Compute the signed distance between two angles in 'turn' units, e.g. 0..1 range.
|
||||
JSC_CCALL(math_angledist,
|
||||
double a1 = js2number(js,argv[0]);
|
||||
double a2 = js2number(js,argv[1]);
|
||||
@@ -150,6 +156,7 @@ JSC_CCALL(math_angledist,
|
||||
return number2js(js,dist);
|
||||
)
|
||||
|
||||
// Apply a random +/- percentage noise to a number. Example: jitter(100, 0.05) -> ~95..105.
|
||||
JSC_CCALL(math_jitter,
|
||||
double n = js2number(js,argv[0]);
|
||||
double pct = js2number(js,argv[1]);
|
||||
@@ -158,6 +165,7 @@ JSC_CCALL(math_jitter,
|
||||
// return number2js(js,n + (rand_range(js,-pct,pct)*n));
|
||||
)
|
||||
|
||||
// Compute the arithmetic mean of an array of numbers.
|
||||
JSC_CCALL(math_mean,
|
||||
double len = JS_ArrayLength(js,argv[0]);
|
||||
double sum = 0;
|
||||
@@ -167,6 +175,7 @@ JSC_CCALL(math_mean,
|
||||
return number2js(js,sum/len);
|
||||
)
|
||||
|
||||
// Sum all elements of an array of numbers.
|
||||
JSC_CCALL(math_sum,
|
||||
double sum = 0.0;
|
||||
int len = JS_ArrayLength(js,argv[0]);
|
||||
@@ -176,6 +185,7 @@ JSC_CCALL(math_sum,
|
||||
return number2js(js,sum);
|
||||
)
|
||||
|
||||
// Compute standard deviation of an array of numbers.
|
||||
JSC_CCALL(math_sigma,
|
||||
int len = JS_ArrayLength(js,argv[0]);
|
||||
double sum = 0;
|
||||
@@ -190,6 +200,7 @@ JSC_CCALL(math_sigma,
|
||||
return number2js(js,sqrt(sum/len));
|
||||
)
|
||||
|
||||
// Compute the median of an array of numbers.
|
||||
JSC_CCALL(math_median,
|
||||
int len = JS_ArrayLength(js,argv[0]);
|
||||
double vals[len];
|
||||
@@ -213,8 +224,10 @@ JSC_CCALL(math_median,
|
||||
return number2js(js,vals[len/2]);
|
||||
)
|
||||
|
||||
// Return the length of a vector (i.e. sqrt of sum of squares).
|
||||
JSC_CCALL(math_length, return number2js(js,arr_vec_length(js,argv[0])); )
|
||||
|
||||
// Return an array of points from a start to an end, spaced out by a certain distance.
|
||||
JSC_CCALL(math_from_to,
|
||||
double start = js2number(js,argv[0]);
|
||||
double end = js2number(js,argv[1]);
|
||||
@@ -232,7 +245,7 @@ JSC_CCALL(math_from_to,
|
||||
return jsarr;
|
||||
)
|
||||
|
||||
|
||||
// Compute the dot product between two numeric arrays, returning a scalar. Extra elements are ignored.
|
||||
JSC_CCALL(math_dot,
|
||||
size_t alen, blen;
|
||||
float *a = js2floats(js,argv[0], &alen);
|
||||
@@ -247,6 +260,7 @@ JSC_CCALL(math_dot,
|
||||
return number2js(js,dot);
|
||||
)
|
||||
|
||||
// Project one vector onto another, returning a new array of the same dimension.
|
||||
JSC_CCALL(math_project,
|
||||
size_t alen, blen;
|
||||
float *a = js2floats(js, argv[0], &alen);
|
||||
@@ -292,6 +306,7 @@ JSC_CCALL(math_project,
|
||||
free(proj);
|
||||
)
|
||||
|
||||
// Compute the midpoint of two arrays of numbers. Only the first two entries are used if 2D is intended.
|
||||
JSC_CCALL(math_midpoint,
|
||||
size_t alen, blen;
|
||||
float *a = js2floats(js, argv[0], &alen);
|
||||
@@ -327,6 +342,7 @@ JSC_CCALL(math_midpoint,
|
||||
free(m);
|
||||
)
|
||||
|
||||
// Reflect a vector across a plane normal. Both arguments must be numeric arrays.
|
||||
JSC_CCALL(math_reflect,
|
||||
size_t alen, blen;
|
||||
float *a = js2floats(js, argv[0], &alen);
|
||||
@@ -371,6 +387,7 @@ JSC_CCALL(math_reflect,
|
||||
free(result);
|
||||
)
|
||||
|
||||
// Compute the normalized direction vector from the first array to the second.
|
||||
JSC_CCALL(math_direction,
|
||||
size_t alen, blen;
|
||||
float *a = js2floats(js, argv[0], &alen);
|
||||
@@ -416,6 +433,7 @@ JSC_CCALL(math_direction,
|
||||
free(dir);
|
||||
)
|
||||
|
||||
// Given a 2D vector, return its angle from the X-axis in radians or some chosen units.
|
||||
JSC_CCALL(math_angle,
|
||||
size_t len;
|
||||
float *v = js2floats(js, argv[0], &len);
|
||||
@@ -431,6 +449,7 @@ JSC_CCALL(math_angle,
|
||||
free(v);
|
||||
)
|
||||
|
||||
// Compute the Euclidean distance between two numeric arrays of matching length.
|
||||
JSC_CCALL(math_distance,
|
||||
size_t alen, blen;
|
||||
float *a = js2floats(js, argv[0], &alen);
|
||||
|
||||
166
resources.cm
Normal file
166
resources.cm
Normal file
@@ -0,0 +1,166 @@
|
||||
var io = use('cellfs')
|
||||
|
||||
Object.defineProperty(Function.prototype, "hashify", {
|
||||
value: function () {
|
||||
var hash = {}
|
||||
var fn = this
|
||||
function hashified(...args) {
|
||||
var key = args[0]
|
||||
if (hash[key] == null) hash[key] = fn(...args)
|
||||
return hash[key]
|
||||
}
|
||||
return hashified
|
||||
},
|
||||
})
|
||||
|
||||
// Merge of the old resources.js and packer.js functionalities
|
||||
var Resources = {}
|
||||
|
||||
// Recognized resource extensions
|
||||
Resources.scripts = ["js"]
|
||||
Resources.images = ["qoi", "png", "gif", "jpg", "jpeg", "ase", "aseprite"]
|
||||
Resources.sounds = ["wav", "flac", "mp3", "qoa"]
|
||||
Resources.fonts = ["ttf"]
|
||||
|
||||
// Helper function: get extension from path in lowercase (e.g., "image.png" -> "png")
|
||||
function getExtension(path) {
|
||||
var idx = path.lastIndexOf('.')
|
||||
if (idx < 0) return ''
|
||||
return path.substring(idx + 1).toLowerCase()
|
||||
}
|
||||
|
||||
// Return true if ext is in at least one of the recognized lists
|
||||
function isRecognizedExtension(ext) {
|
||||
if (!ext) return false
|
||||
if (Resources.scripts.includes(ext)) return true
|
||||
if (Resources.images.includes(ext)) return true
|
||||
if (Resources.sounds.includes(ext)) return true
|
||||
if (Resources.fonts.includes(ext)) return true
|
||||
if (Resources.lib.includes('.' + ext)) return true // for .so or .dll
|
||||
return false
|
||||
}
|
||||
|
||||
function find_in_path(filename, exts = []) {
|
||||
if (typeof filename != 'string') return null
|
||||
|
||||
if (filename.includes('.')) {
|
||||
var candidate = filename // possibly need "/" ?
|
||||
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
|
||||
return null
|
||||
}
|
||||
|
||||
// Only check extensions if exts is provided and not empty
|
||||
if (exts.length > 0) {
|
||||
for (var ext of exts) {
|
||||
var candidate = filename + '.' + ext
|
||||
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
|
||||
}
|
||||
} else {
|
||||
// Fallback to extensionless file only if no extensions are specified
|
||||
var candidate = filename
|
||||
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Return a canonical path (the real directory plus the path)
|
||||
Resources.canonical = function(file) {
|
||||
return io.realdir(file) + file
|
||||
}
|
||||
|
||||
// The resource finders
|
||||
Resources.find_image = function(file) {
|
||||
return find_in_path(file, Resources.images)
|
||||
}.hashify()
|
||||
|
||||
Resources.find_sound = function(file) {
|
||||
return find_in_path(file, Resources.sounds)
|
||||
}.hashify()
|
||||
|
||||
Resources.find_script = function(file) {
|
||||
return find_in_path(file, Resources.scripts)
|
||||
}.hashify()
|
||||
|
||||
Resources.find_font = function(file) {
|
||||
return find_in_path(file, Resources.fonts)
|
||||
}.hashify()
|
||||
|
||||
// .prosperonignore reading helper
|
||||
function read_ignore(dir) {
|
||||
var path = dir + '/.prosperonignore'
|
||||
var patterns = []
|
||||
if (io.exists(path)) {
|
||||
var lines = io.slurp(path).split('\n')
|
||||
for (var line of lines) {
|
||||
line = line.trim()
|
||||
if (!line || line.startsWith('#')) continue
|
||||
patterns.push(line)
|
||||
}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
// Return a list of recognized files in the directory (and subdirectories),
|
||||
// skipping those matched by .prosperonignore. Directory paths are skipped.
|
||||
Resources.getAllFiles = function(dir = "") {
|
||||
var patterns = read_ignore(dir)
|
||||
var all = io.globfs(patterns, dir)
|
||||
var results = []
|
||||
for (var f of all) {
|
||||
var fullPath = dir + '/' + f
|
||||
try {
|
||||
var st = io.stat(fullPath)
|
||||
// skip directories (filesize=0) or unrecognized extension
|
||||
if (!st.filesize) continue
|
||||
var ext = getExtension(f)
|
||||
if (!isRecognizedExtension(ext)) continue
|
||||
results.push(fullPath)
|
||||
} catch(e) {}
|
||||
}
|
||||
return results
|
||||
}
|
||||
Resources.getAllFiles[cell.DOC] = `
|
||||
Return a list of recognized files in the given directory that are not matched by
|
||||
.prosperonignore, skipping directories. Recognized extensions include scripts,
|
||||
images, sounds, fonts, and libs.
|
||||
|
||||
:param dir: The directory to search.
|
||||
:return: An array of recognized file paths.
|
||||
`
|
||||
|
||||
// Categorize files by resource type
|
||||
Resources.gatherStats = function(filePaths) {
|
||||
var stats = {
|
||||
scripts:0, images:0, sounds:0, fonts:0, lib:0, other:0, total:filePaths.length
|
||||
}
|
||||
for (var path of filePaths) {
|
||||
var ext = getExtension(path)
|
||||
if (Resources.scripts.includes(ext)) {
|
||||
stats.scripts++
|
||||
continue
|
||||
}
|
||||
if (Resources.images.includes(ext)) {
|
||||
stats.images++
|
||||
continue
|
||||
}
|
||||
if (Resources.sounds.includes(ext)) {
|
||||
stats.sounds++
|
||||
continue
|
||||
}
|
||||
if (Resources.fonts.includes(ext)) {
|
||||
stats.fonts++
|
||||
continue
|
||||
}
|
||||
stats.other++
|
||||
}
|
||||
return stats
|
||||
}
|
||||
Resources.gatherStats[cell.DOC] = `
|
||||
Analyze a list of recognized files and categorize them by scripts, images, sounds,
|
||||
fonts, libs, or other. Return a stats object with these counts and the total.
|
||||
|
||||
:param filePaths: An array of file paths to analyze.
|
||||
:return: { scripts, images, sounds, fonts, lib, other, total }
|
||||
`
|
||||
|
||||
return Resources
|
||||
Reference in New Issue
Block a user