Compare commits

...

97 Commits

Author SHA1 Message Date
John Alanbrook
b88d22aefc fix find/search error for mask 2026-02-26 16:40:38 -06:00
John Alanbrook
9147db6fdc more examples 2026-02-26 15:54:11 -06:00
John Alanbrook
ce6b0ddb3a rm push/pop 2026-02-26 08:13:27 -06:00
John Alanbrook
ef7e3e6449 fix clay 2026-02-26 00:56:25 -06:00
John Alanbrook
0262ed7388 fix gc bug 2026-02-25 23:26:19 -06:00
John Alanbrook
a840f32c4e fix missing shader 2026-02-25 22:13:42 -06:00
John Alanbrook
29818b1b0b fix examples 2026-02-25 16:58:06 -06:00
John Alanbrook
250f535abe rm dupavlue and freevalue 2026-02-25 09:52:06 -06:00
John Alanbrook
20376aa5e4 rm dupavlue and freevalue 2026-02-25 09:41:27 -06:00
John Alanbrook
3d87fdeb5f probe 2026-02-24 21:08:46 -06:00
John Alanbrook
f87854fca1 update 2026-02-23 19:46:09 -06:00
John Alanbrook
2fc5e2db3f add blog to website 2026-02-23 18:21:42 -06:00
John Alanbrook
c15919074c gitignore 2026-02-23 18:10:57 -06:00
John Alanbrook
83b798e365 Add Hugo website and rewrite docs to match current engine
New Hugo site in website/ with prosperon.dev theme (blue/gold/castle
aesthetic), docs sidebar navigation, and content pages. Rewrote all
doc files to align with the actual codebase: compositor+film2d
rendering, use() modules (no global prosperon object), Pit language,
script+JSON entity model. Added entities.md, front matter to all
70+ API docs, and updated API index for current module architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:09:55 -06:00
John Alanbrook
1619122a58 fixes 2026-02-23 11:32:45 -06:00
John Alanbrook
ff2e4bd578 fix prosperon layout 2026-02-20 21:23:21 -06:00
John Alanbrook
e78faccab8 fix syntax 2026-02-17 16:03:43 -06:00
John Alanbrook
4e1b63fd0e fix syntax 2026-02-17 09:15:15 -06:00
John Alanbrook
f310c18b84 new isfunction api: 2026-01-26 11:48:15 -06:00
John Alanbrook
6c07f11992 new 2026-01-24 09:56:07 -06:00
John Alanbrook
4f7e7ab591 fix no getset 2026-01-23 11:28:27 -06:00
John Alanbrook
dd31a1c2b0 constructors 2026-01-21 19:41:20 -06:00
John Alanbrook
f7be9c3344 push 2026-01-21 09:05:02 -06:00
John Alanbrook
18ca9e14ba rm new; rm String 2026-01-20 20:09:27 -06:00
John Alanbrook
313a2e7eeb rm new 2026-01-20 12:04:30 -06:00
John Alanbrook
187879a7c6 rm new 2026-01-20 08:36:55 -06:00
John Alanbrook
6e78e8f9c2 rm break 2026-01-20 05:25:40 -06:00
John Alanbrook
027435d193 rm for ... in 2026-01-19 18:57:25 -06:00
John Alanbrook
1d78e725bb rm for ... in 2026-01-19 18:57:11 -06:00
John Alanbrook
86530871e6 rm of 2026-01-19 01:06:51 -06:00
John Alanbrook
a199278e7d length 2026-01-18 11:22:52 -06:00
John Alanbrook
23dc5820ee replace 2026-01-18 08:55:22 -06:00
John Alanbrook
92b1252c82 misty 2026-01-16 20:57:43 -06:00
John Alanbrook
22962bbd63 misty 2026-01-16 20:56:16 -06:00
John Alanbrook
9f6435ece9 rm scene 2026-01-14 09:57:00 -06:00
John Alanbrook
f591fff8ac rm new 2026-01-13 22:16:05 -06:00
John Alanbrook
548bad0c57 drawdd 2026-01-10 10:43:44 -06:00
John Alanbrook
6e752e5a06 draw2d 2026-01-09 23:35:53 -06:00
John Alanbrook
56cad63541 draw2d 2026-01-08 21:00:44 -06:00
John Alanbrook
66e1dda563 line2d 2026-01-08 18:15:18 -06:00
John Alanbrook
ce479299eb shape2d 2026-01-08 17:55:50 -06:00
John Alanbrook
dec26f6b41 enhanced 2d drawables 2026-01-08 13:08:14 -06:00
John Alanbrook
1ed5562650 graphics 2026-01-08 12:53:05 -06:00
John Alanbrook
9ee3428578 update 2026-01-08 09:10:37 -06:00
John Alanbrook
4ea9d43a94 fix 2026-01-07 14:25:11 -06:00
John Alanbrook
0522b967ca rework 2026-01-06 20:25:55 -06:00
John Alanbrook
50bee7a5c0 world 2026-01-03 08:25:55 -06:00
John Alanbrook
249b78d141 scene 2026-01-01 22:01:58 -06:00
John Alanbrook
b61b85c3a8 number 2025-12-30 02:23:29 -06:00
John Alanbrook
813d4e771c sprite vert 2025-12-29 20:46:42 -06:00
John Alanbrook
837659fcdc imgui 2025-12-27 13:15:21 -06:00
John Alanbrook
37c360dc67 accumulator blur 2025-12-24 15:55:44 -06:00
John Alanbrook
971299f062 multiple effects 2025-12-24 14:36:54 -06:00
John Alanbrook
03159c66d7 add pos to compositor layers 2025-12-24 14:19:52 -06:00
John Alanbrook
3beb5f7091 fix disabled presentation 2025-12-24 14:06:06 -06:00
John Alanbrook
5c79173fa5 compositor 2025-12-24 13:38:09 -06:00
John Alanbrook
9d2d1a8498 sdf 2025-12-24 11:44:15 -06:00
John Alanbrook
e5232a3009 sdf 2025-12-23 14:40:27 -06:00
John Alanbrook
54f8c6539d sdf 2025-12-23 08:51:34 -06:00
John Alanbrook
4a29a49f28 clay 2 2025-12-23 00:53:00 -06:00
John Alanbrook
2d6fa94db1 tiling 2025-12-22 22:21:02 -06:00
John Alanbrook
3ee8ff8cf4 text rendering 2025-12-22 17:44:23 -06:00
John Alanbrook
22adec77cd fx graph 2025-12-22 16:21:21 -06:00
John Alanbrook
3525a1610b compositor 2025-12-21 18:07:41 -06:00
John Alanbrook
802ab0dc14 compositor 2025-12-21 10:37:27 -06:00
John Alanbrook
3d9b7342e2 remove model 2025-12-19 16:44:37 -06:00
John Alanbrook
45e2fc7f2c improve 2025-12-19 16:42:40 -06:00
John Alanbrook
4bdbde52a0 private vars 2025-12-19 00:10:19 -06:00
John Alanbrook
0bc0472a2f no more js 2025-12-17 00:48:19 -06:00
John Alanbrook
b0f37a6a9c sound system 2025-12-11 14:42:44 -06:00
John Alanbrook
f6977b8864 docs 2025-12-10 15:19:13 -06:00
John Alanbrook
19880c6826 move cell toml 2025-12-08 11:39:19 -06:00
John Alanbrook
5a0949058a no nee for stb_ds implementation 2025-12-07 15:35:00 -06:00
John Alanbrook
a82e93dabf add sdl header 2025-12-06 12:29:03 -06:00
John Alanbrook
4e6f64a28e remove dmon 2025-12-05 15:38:23 -06:00
John Alanbrook
1395d4b630 fix internal refs 2025-12-04 21:54:57 -06:00
John Alanbrook
49b437687d reobust classes 2025-12-04 20:15:44 -06:00
John Alanbrook
52a72aeee4 update surface path 2025-12-04 05:46:31 -06:00
John Alanbrook
0b4b9f1fb7 fix blob get 2025-12-03 23:18:49 -06:00
John Alanbrook
f56a7e92e0 remove const 2025-12-03 22:46:44 -06:00
John Alanbrook
80a6e8ad26 who knows 2025-12-01 18:09:38 -06:00
John Alanbrook
345ddebfc7 audio processing 2025-12-01 09:55:52 -06:00
John Alanbrook
91626b8efd add angle funcs 2025-12-01 08:46:45 -06:00
John Alanbrook
91d7d6f1df remove qjs_macros.h 2025-11-30 17:28:42 -06:00
John Alanbrook
079ce7ccc1 imgui compiles into prosperon 2025-11-30 15:31:15 -06:00
John Alanbrook
c1db96126e dmon links on macos 2025-11-30 14:59:53 -06:00
John Alanbrook
560e62d568 move renderer to sdl; add simplex 2025-11-30 14:43:57 -06:00
John Alanbrook
c1534dfe44 remove unnneded headers; reorganization 2025-11-30 01:20:14 -06:00
John Alanbrook
5fd83c6928 cleanup 2025-11-30 00:17:04 -06:00
John Alanbrook
36930ef007 remove cell doc 2025-11-29 23:44:28 -06:00
John Alanbrook
4f97a3f18e add graphics back 2025-11-29 17:24:27 -06:00
John Alanbrook
7502f8d5b1 md 2025-11-29 12:41:10 -06:00
John Alanbrook
db82e0b370 mod 2025-11-26 20:26:31 -06:00
John Alanbrook
f1cec89f9e add many source files 2025-11-26 12:52:56 -06:00
John Alanbrook
bcfff84461 Remove steam 2025-11-25 21:06:14 -06:00
John Alanbrook
7aa49c9b76 use physfs; proper namespace resolution 2025-11-25 09:57:42 -06:00
John Alanbrook
b7616c61e2 fix modules 2025-11-24 23:09:51 -06:00
657 changed files with 131091 additions and 5295 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

2019
HandmadeMath.c Normal file

File diff suppressed because it is too large Load Diff

673
HandmadeMath.h Normal file
View File

@@ -0,0 +1,673 @@
/*
HandmadeMath.h v2.0.0
This is a single header file with a bunch of useful types and functions for
games and graphics. Consider it a lightweight alternative to GLM that works
both C and C++.
=============================================================================
CONFIG
=============================================================================
By default, all angles in Handmade Math are specified in radians. However, it
can be configured to use degrees or turns instead. Use one of the following
defines to specify the default unit for angles:
#define HANDMADE_MATH_USE_RADIANS
#define HANDMADE_MATH_USE_DEGREES
#define HANDMADE_MATH_USE_TURNS
Regardless of the default angle, you can use the following functions to
specify an angle in a particular unit:
HMM_AngleRad(radians)
HMM_AngleDeg(degrees)
HMM_AngleTurn(turns)
The definitions of these functions change depending on the default unit.
-----------------------------------------------------------------------------
Handmade Math ships with SSE (SIMD) implementations of several common
operations. To disable the use of SSE intrinsics, you must define
HANDMADE_MATH_NO_SSE before including this file:
#define HANDMADE_MATH_NO_SSE
#include "HandmadeMath.h"
-----------------------------------------------------------------------------
To use Handmade Math without the C runtime library, you must provide your own
implementations of basic math functions. Otherwise, HandmadeMath.h will use
the runtime library implementation of these functions.
Define HANDMADE_MATH_PROVIDE_MATH_FUNCTIONS and provide your own
implementations of HMM_SINF, HMM_COSF, HMM_TANF, HMM_ACOSF, and HMM_SQRTF
before including HandmadeMath.h, like so:
#define HANDMADE_MATH_PROVIDE_MATH_FUNCTIONS
#define HMM_SINF MySinF
#define HMM_COSF MyCosF
#define HMM_TANF MyTanF
#define HMM_ACOSF MyACosF
#define HMM_SQRTF MySqrtF
#include "HandmadeMath.h"
By default, it is assumed that your math functions take radians. To use
different units, you must define HMM_ANGLE_USER_TO_INTERNAL and
HMM_ANGLE_INTERNAL_TO_USER. For example, if you want to use degrees in your
code but your math functions use turns:
#define HMM_ANGLE_USER_TO_INTERNAL(a) ((a)*HMM_DegToTurn)
#define HMM_ANGLE_INTERNAL_TO_USER(a) ((a)*HMM_TurnToDeg)
=============================================================================
LICENSE
This software is in the public domain. Where that dedictaion is not
recognized, you are granted a perpetual, irrevocable license to copy,
distribute, and modify this file as you see fit.
=============================================================================
CREDITS
Originally written by Zakary Strange.
Functionality:
Zakary Strange (strangezak@protonmail.com && @strangezak)
Matt Mascarenhas (@miblo_)
Aleph
FieryDrake (@fierydrake)
Gingerbill (@TheGingerBill)
Ben Visness (@bvisness)
Trinton Bullard (@Peliex_Dev)
@AntonDan
Logan Forman (@dev_dwarf)
Fixes:
Jeroen van Rijn (@J_vanRijn)
Kiljacken (@Kiljacken)
Insofaras (@insofaras)
Daniel Gibson (@DanielGibson)
*/
#ifndef HANDMADE_MATH_H
#define HANDMADE_MATH_H
#if !defined(HANDMADE_MATH_NO_SIMD)
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
#define HANDMADE_MATH__USE_NEON 1
#include <arm_neon.h>
#elif defined(_MSC_VER)
#if defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 1)
#define HANDMADE_MATH__USE_SSE 1
#include <xmmintrin.h>
#endif
#elif defined(__SSE__)
#define HANDMADE_MATH__USE_SSE 1
#include <xmmintrin.h>
#endif
#endif
#ifdef _MSC_VER
#pragma warning(disable : 4201)
#endif
#if defined(__GNUC__) || defined(__clang__)
#define HMM_DEPRECATED(msg) __attribute__((deprecated(msg)))
#elif defined(_MSC_VER)
#define HMM_DEPRECATED(msg) __declspec(deprecated(msg))
#else
#define HMM_DEPRECATED(msg)
#endif
#if !defined(HANDMADE_MATH_USE_DEGREES) && !defined(HANDMADE_MATH_USE_TURNS) && !defined(HANDMADE_MATH_USE_RADIANS)
#define HANDMADE_MATH_USE_RADIANS
#endif
#define HMM_PI 3.14159265358979323846
#define HMM_PI32 3.14159265359f
#define HMM_DEG180 180.0
#define HMM_DEG18032 180.0f
#define HMM_TURNHALF 0.5
#define HMM_TURNHALF32 0.5f
#define HMM_RadToDeg ((float)(HMM_DEG180 / HMM_PI))
#define HMM_RadToTurn ((float)(HMM_TURNHALF / HMM_PI))
#define HMM_DegToRad ((float)(HMM_PI / HMM_DEG180))
#define HMM_DegToTurn ((float)(HMM_TURNHALF / HMM_DEG180))
#define HMM_TurnToRad ((float)(HMM_PI / HMM_TURNHALF))
#define HMM_TurnToDeg ((float)(HMM_DEG180 / HMM_TURNHALF))
#if defined(HANDMADE_MATH_USE_RADIANS)
#define HMM_AngleRad(a) (a)
#define HMM_AngleDeg(a) ((a)*HMM_DegToRad)
#define HMM_AngleTurn(a) ((a)*HMM_TurnToRad)
#elif defined(HANDMADE_MATH_USE_DEGREES)
#define HMM_AngleRad(a) ((a)*HMM_RadToDeg)
#define HMM_AngleDeg(a) (a)
#define HMM_AngleTurn(a) ((a)*HMM_TurnToDeg)
#elif defined(HANDMADE_MATH_USE_TURNS)
#define HMM_AngleRad(a) ((a)*HMM_RadToTurn)
#define HMM_AngleDeg(a) ((a)*HMM_DegToTurn)
#define HMM_AngleTurn(a) (a)
#endif
#if !defined(HANDMADE_MATH_PROVIDE_MATH_FUNCTIONS)
#include <math.h>
#define HMM_SINF sinf
#define HMM_COSF cosf
#define HMM_TANF tanf
#define HMM_SQRTF sqrtf
#define HMM_ACOSF acosf
#endif
#if !defined(HMM_ANGLE_USER_TO_INTERNAL)
#define HMM_ANGLE_USER_TO_INTERNAL(a) (HMM_ToRad(a))
#endif
#if !defined(HMM_ANGLE_INTERNAL_TO_USER)
#if defined(HANDMADE_MATH_USE_RADIANS)
#define HMM_ANGLE_INTERNAL_TO_USER(a) (a)
#elif defined(HANDMADE_MATH_USE_DEGREES)
#define HMM_ANGLE_INTERNAL_TO_USER(a) ((a)*HMM_RadToDeg)
#elif defined(HANDMADE_MATH_USE_TURNS)
#define HMM_ANGLE_INTERNAL_TO_USER(a) ((a)*HMM_RadToTurn)
#endif
#endif
#define HMM_MIN(a, b) ((a) > (b) ? (b) : (a))
#define HMM_MAX(a, b) ((a) < (b) ? (b) : (a))
#define HMM_ABS(a) ((a) > 0 ? (a) : -(a))
#define HMM_MOD(a, m) (((a) % (m)) >= 0 ? ((a) % (m)) : (((a) % (m)) + (m)))
#define HMM_SQUARE(x) ((x) * (x))
#define HMMFMT_VEC3 "[%g,%g,%g]"
#define HMMPRINT_VEC3(vec) vec.x, vec.y, vec.z
#define FMT_VEC4 "[%g,%g,%g,%g]"
#define PRINT_VEC4(vec) vec.x, vec.y, vec.z, vec.w
#define FMT_M4 "[%g,%g,%g,%g\n%g,%g,%g,%g\n%g,%g,%g,%g\n%g,%g,%g,%g]"
#define PRINT_M4(m) m.e[0][0], m.e[0][1], m.e[0][2], m.e[0][3], m.e[1][0], m.e[1][1], m.e[1][2], m.e[1][3], m.e[2][0], m.e[2][1], m.e[2][2], m.e[2][3], m.e[3][0], m.e[3][1], m.e[3][2], m.e[3][3]
typedef union HMM_Vec2 {
struct
{
float X, Y;
};
struct {
float x, y;
};
struct
{
float U, V;
};
struct
{
float Left, Right;
};
struct
{
float Width, Height;
};
float Elements[2];
float e[2];
} HMM_Vec2;
typedef union HMM_Vec3 {
struct
{
float X, Y, Z;
};
struct { float x, y, z; };
struct
{
float U, V, W;
};
struct
{
float R, G, B;
};
struct
{
HMM_Vec2 XY;
float _Ignored0;
};
struct
{
HMM_Vec2 xy;
float _Ignored4;
};
struct
{
float _Ignored1;
HMM_Vec2 YZ;
};
struct
{
HMM_Vec2 UV;
float _Ignored2;
};
struct
{
float _Ignored3;
HMM_Vec2 VW;
};
struct
{
HMM_Vec2 cp;
float _Ignored5;
};
float Elements[3];
float e[3];
} HMM_Vec3;
typedef union HMM_Quat {
struct
{
union {
HMM_Vec3 XYZ;
struct
{
float X, Y, Z;
};
};
float W;
};
struct {float x, y, z, w;};
float Elements[4];
float e[4];
#ifdef HANDMADE_MATH__USE_SSE
__m128 SSE;
#endif
} HMM_Quat;
typedef union HMM_Vec4 {
struct
{
union {
HMM_Vec3 XYZ;
struct
{
float X, Y, Z;
};
HMM_Vec3 xyz;
};
float W;
};
struct
{
union {
HMM_Vec3 RGB;
struct
{
float R, G, B;
};
};
float A;
};
struct
{
HMM_Vec2 XY;
float _Ignored0;
float _Ignored1;
};
struct {
HMM_Vec2 xy;
float _ig0;
float _ig1;
};
struct {
HMM_Vec2 _2;
float _ig2;
float _ig3;
};
struct {
HMM_Vec3 _3;
float _ig4;
};
struct
{
float _Ignored2;
HMM_Vec2 YZ;
float _Ignored3;
};
struct
{
float _Ignored4;
float _Ignored5;
HMM_Vec2 ZW;
};
struct
{
HMM_Vec2 cp;
HMM_Vec2 wh;
};
HMM_Quat quat;
struct {float x, y, z, w; };
struct {float r, g, b, a; };
struct {float u0, u1, v0, v1;};
float Elements[4];
float e[4];
#ifdef HANDMADE_MATH__USE_SSE
__m128 SSE;
#endif
#ifdef HANDMADE_MATH__USE_NEON
float32x4_t NEON;
#endif
} HMM_Vec4;
typedef union HMM_Mat2 {
float Elements[2][2];
HMM_Vec2 Columns[2];
} HMM_Mat2;
typedef union HMM_Mat3 {
float Elements[3][3];
HMM_Vec3 Columns[3];
} HMM_Mat3;
typedef union HMM_Mat4 {
float Elements[4][4];
HMM_Vec4 Columns[4];
HMM_Vec4 col[4];
float e[4][4];
float em[16];
} HMM_Mat4;
extern const HMM_Vec2 v2zero;
extern const HMM_Vec2 v2one;
extern const HMM_Vec3 v3zero;
extern const HMM_Vec3 v3one;
extern const HMM_Vec4 v4zero;
typedef signed int HMM_Bool;
extern const HMM_Vec3 vX;
extern const HMM_Vec3 vY;
extern const HMM_Vec3 vZ;
extern const HMM_Vec3 vUP;
extern const HMM_Vec3 vDOWN;
extern const HMM_Vec3 vFWD;
extern const HMM_Vec3 vBKWD;
extern const HMM_Vec3 vLEFT;
extern const HMM_Vec3 vRIGHT;
extern const HMM_Mat4 MAT1;
extern const HMM_Quat QUAT1;
/*
* Angle unit conversion functions
*/
float HMM_ToRad(float Angle);
float HMM_ToDeg(float Angle);
float HMM_ToTurn(float Angle);
/*
* Floating-point math functions
*/
float HMM_SinF(float Angle);
float HMM_CosF(float Angle);
float HMM_TanF(float Angle);
float HMM_ACosF(float Arg);
float HMM_SqrtF(float Float);
float HMM_InvSqrtF(float Float);
/*
* Utility functions
*/
float HMM_Lerp(float A, float Time, float B);
float HMM_Clamp(float Min, float Value, float Max);
float frand(float max);
/*
* Vector initialization
*/
HMM_Vec2 HMM_V2(float X, float Y);
HMM_Vec3 HMM_V3(float X, float Y, float Z);
HMM_Vec3 HMM_V3i(float i);
HMM_Vec4 HMM_V4(float X, float Y, float Z, float W);
HMM_Vec4 HMM_V4V(HMM_Vec3 Vector, float W);
/*
* Binary vector operations
*/
HMM_Vec2 HMM_AddV2(HMM_Vec2 Left, HMM_Vec2 Right);
HMM_Vec3 HMM_AddV3(HMM_Vec3 Left, HMM_Vec3 Right);
HMM_Vec4 HMM_AddV4(HMM_Vec4 Left, HMM_Vec4 Right);
HMM_Vec2 HMM_SubV2(HMM_Vec2 Left, HMM_Vec2 Right);
HMM_Vec3 HMM_SubV3(HMM_Vec3 Left, HMM_Vec3 Right);
HMM_Vec4 HMM_SubV4(HMM_Vec4 Left, HMM_Vec4 Right);
HMM_Vec2 HMM_MulV2(HMM_Vec2 Left, HMM_Vec2 Right);
HMM_Vec2 HMM_MulV2F(HMM_Vec2 Left, float Right);
HMM_Vec3 HMM_MulV3(HMM_Vec3 Left, HMM_Vec3 Right);
HMM_Vec3 HMM_MulV3F(HMM_Vec3 Left, float Right);
HMM_Vec4 HMM_MulV4(HMM_Vec4 Left, HMM_Vec4 Right);
HMM_Vec4 HMM_MulV4F(HMM_Vec4 Left, float Right);
HMM_Vec2 HMM_DivV2(HMM_Vec2 Left, HMM_Vec2 Right);
HMM_Vec2 HMM_DivV2F(HMM_Vec2 Left, float Right);
HMM_Vec3 HMM_DivV3(HMM_Vec3 Left, HMM_Vec3 Right);
HMM_Vec3 HMM_DivV3F(HMM_Vec3 Left, float Right);
HMM_Vec4 HMM_DivV4(HMM_Vec4 Left, HMM_Vec4 Right);
HMM_Vec4 HMM_DivV4F(HMM_Vec4 Left, float Right);
HMM_Bool HMM_EqV2(HMM_Vec2 Left, HMM_Vec2 Right);
HMM_Bool HMM_EqV3(HMM_Vec3 Left, HMM_Vec3 Right);
HMM_Bool HMM_EqV4(HMM_Vec4 Left, HMM_Vec4 Right);
float HMM_DotV2(HMM_Vec2 Left, HMM_Vec2 Right);
HMM_Vec2 HMM_ProjV2(HMM_Vec2 a, HMM_Vec2 b);
float HMM_DotV3(HMM_Vec3 Left, HMM_Vec3 Right);
float HMM_DotV4(HMM_Vec4 Left, HMM_Vec4 Right);
HMM_Vec3 HMM_Cross(HMM_Vec3 Left, HMM_Vec3 Right);
/*
* Unary vector operations
*/
float HMM_LenSqrV2(HMM_Vec2 A);
float HMM_LenSqrV3(HMM_Vec3 A);
float HMM_LenSqrV4(HMM_Vec4 A);
float HMM_LenV2(HMM_Vec2 A);
float HMM_AngleV2(HMM_Vec2 a, HMM_Vec2 b);
float HMM_DistV2(HMM_Vec2 a, HMM_Vec2 b);
HMM_Vec2 HMM_V2Rotate(HMM_Vec2 v, float angle);
float HMM_LenV3(HMM_Vec3 A);
float HMM_DistV3(HMM_Vec3 a, HMM_Vec3 b);
float HMM_AngleV3(HMM_Vec3 a, HMM_Vec3 b);
float HMM_LenV4(HMM_Vec4 A);
float HMM_AngleV4(HMM_Vec4 a, HMM_Vec4 b);
HMM_Vec2 HMM_NormV2(HMM_Vec2 A);
HMM_Vec3 HMM_NormV3(HMM_Vec3 A);
HMM_Vec4 HMM_NormV4(HMM_Vec4 A);
/*
* Utility vector functions
*/
HMM_Vec2 HMM_LerpV2(HMM_Vec2 A, float Time, HMM_Vec2 B);
HMM_Vec3 HMM_LerpV3(HMM_Vec3 A, float Time, HMM_Vec3 B);
HMM_Vec4 HMM_LerpV4(HMM_Vec4 A, float Time, HMM_Vec4 B);
/*
* SSE stuff
*/
HMM_Vec4 HMM_LinearCombineV4M4(HMM_Vec4 Left, HMM_Mat4 Right);
/*
* 2x2 Matrices
*/
HMM_Mat2 HMM_M2(void);
HMM_Mat2 HMM_M2D(float Diagonal);
HMM_Mat2 HMM_TransposeM2(HMM_Mat2 Matrix);
HMM_Mat2 HMM_RotateM2(float angle);
HMM_Mat2 HMM_AddM2(HMM_Mat2 Left, HMM_Mat2 Right);
HMM_Mat2 HMM_SubM2(HMM_Mat2 Left, HMM_Mat2 Right);
HMM_Vec2 HMM_MulM2V2(HMM_Mat2 Matrix, HMM_Vec2 Vector);
HMM_Mat2 HMM_MulM2(HMM_Mat2 Left, HMM_Mat2 Right);
HMM_Mat2 HMM_MulM2F(HMM_Mat2 Matrix, float Scalar);
HMM_Mat2 HMM_DivM2F(HMM_Mat2 Matrix, float Scalar);
float HMM_DeterminantM2(HMM_Mat2 Matrix);
HMM_Mat2 HMM_InvGeneralM2(HMM_Mat2 Matrix);
/*
* 3x3 Matrices
*/
HMM_Mat3 HMM_M3(void);
HMM_Mat3 HMM_M3D(float Diagonal);
HMM_Mat3 HMM_Translate2D(HMM_Vec2 p);
HMM_Mat3 HMM_RotateM3(float angle);
HMM_Mat3 HMM_ScaleM3(HMM_Vec2 s);
HMM_Mat3 HMM_TransposeM3(HMM_Mat3 Matrix);
HMM_Mat3 HMM_AddM3(HMM_Mat3 Left, HMM_Mat3 Right);
HMM_Mat3 HMM_SubM3(HMM_Mat3 Left, HMM_Mat3 Right);
HMM_Vec3 HMM_MulM3V3(HMM_Mat3 Matrix, HMM_Vec3 Vector);
HMM_Mat3 HMM_MulM3(HMM_Mat3 Left, HMM_Mat3 Right);
HMM_Mat3 HMM_MulM3F(HMM_Mat3 Matrix, float Scalar);
HMM_Mat3 HMM_DivM3F(HMM_Mat3 Matrix, float Scalar);
HMM_Mat2 HMM_ScaleM2(HMM_Vec2 Scale);
float HMM_DeterminantM3(HMM_Mat3 Matrix);
HMM_Mat3 HMM_M2Basis(HMM_Mat2 basis);
HMM_Mat3 HMM_InvGeneralM3(HMM_Mat3 Matrix);
/*
* 4x4 Matrices
*/
HMM_Mat4 HMM_M4(void);
HMM_Mat4 HMM_M4D(float Diagonal);
HMM_Mat4 HMM_TransposeM4(HMM_Mat4 Matrix);
HMM_Mat4 HMM_AddM4(HMM_Mat4 Left, HMM_Mat4 Right);
HMM_Mat4 HMM_SubM4(HMM_Mat4 Left, HMM_Mat4 Right);
HMM_Mat4 HMM_MulM4(HMM_Mat4 Left, HMM_Mat4 Right);
HMM_Mat4 HMM_MulM4_P(HMM_Mat4 *Left, HMM_Mat4 *Right);
HMM_Mat4 HMM_MulM4F(HMM_Mat4 Matrix, float Scalar);
HMM_Vec4 HMM_MulM4V4(HMM_Mat4 Matrix, HMM_Vec4 Vector);
HMM_Vec4 HMM_MulM4V4_P(HMM_Mat4 *Matrix, HMM_Vec4 *Vector);
HMM_Mat4 HMM_DivM4F(HMM_Mat4 Matrix, float Scalar);
float HMM_DeterminantM4(HMM_Mat4 Matrix);
// Returns a general-purpose inverse of an HMM_Mat4. Note that special-purpose inverses of many transformations
// are available and will be more efficient.
HMM_Mat4 HMM_InvGeneralM4(HMM_Mat4 Matrix);
/*
* Common graphics transformations
*/
// Produces a right-handed orthographic projection matrix with Z ranging from -1 to 1 (the GL convention).
// Left, Right, Bottom, and Top specify the coordinates of their respective clipping planes.
// Near and Far specify the distances to the near and far clipping planes.
HMM_Mat4 HMM_Orthographic_RH_NO(float Left, float Right, float Bottom, float Top, float Near, float Far);
// Produces a right-handed orthographic projection matrix with Z ranging from 0 to 1 (the DirectX convention).
// Left, Right, Bottom, and Top specify the coordinates of their respective clipping planes.
// Near and Far specify the distances to the near and far clipping planes.
HMM_Mat4 HMM_Orthographic_RH_ZO(float Left, float Right, float Bottom, float Top, float Near, float Far);
// Produces a left-handed orthographic projection matrix with Z ranging from -1 to 1 (the GL convention).
// Left, Right, Bottom, and Top specify the coordinates of their respective clipping planes.
// Near and Far specify the distances to the near and far clipping planes.
HMM_Mat4 HMM_Orthographic_LH_NO(float Left, float Right, float Bottom, float Top, float Near, float Far);
// Produces a left-handed orthographic projection matrix with Z ranging from 0 to 1 (the DirectX convention).
// Left, Right, Bottom, and Top specify the coordinates of their respective clipping planes.
// Near and Far specify the distances to the near and far clipping planes.
HMM_Mat4 HMM_Orthographic_LH_ZO(float Left, float Right, float Bottom, float Top, float Near, float Far);
// Returns an inverse for the given orthographic projection matrix. Works for all orthographic
// projection matrices, regardless of handedness or NDC convention.
HMM_Mat4 HMM_InvOrthographic(HMM_Mat4 OrthoMatrix);
HMM_Mat4 HMM_Perspective_RH_NO(float FOV, float AspectRatio, float Near, float Far);
HMM_Mat4 HMM_Perspective_RH_ZO(float FOV, float AspectRatio, float Near, float Far);
HMM_Mat4 HMM_Perspective_LH_NO(float FOV, float AspectRatio, float Near, float Far);
HMM_Mat4 HMM_Perspective_LH_ZO(float FOV, float AspectRatio, float Near, float Far);
HMM_Mat4 HMM_Perspective_Metal(float FOV, float AspectRation, float Near, float Far);
HMM_Mat4 HMM_Orthographic_Metal(float l, float r, float b, float t, float near, float far);
HMM_Mat4 HMM_Orthographic_DX(float l, float r, float b, float t, float near, float far);
HMM_Mat4 HMM_Orthographic_GL(float l, float r, float b, float t, float near, float far);
HMM_Mat4 HMM_InvPerspective_RH(HMM_Mat4 PerspectiveMatrix);
HMM_Mat4 HMM_InvPerspective_LH(HMM_Mat4 PerspectiveMatrix);
HMM_Mat4 HMM_Translate(HMM_Vec3 Translation);
HMM_Mat4 HMM_InvTranslate(HMM_Mat4 TranslationMatrix);
HMM_Mat4 HMM_Rotate_RH(float Angle, HMM_Vec3 Axis);
HMM_Mat4 HMM_Rotate_LH(float Angle, HMM_Vec3 Axis);
HMM_Mat4 HMM_InvRotate(HMM_Mat4 RotationMatrix);
HMM_Mat4 HMM_Scale(HMM_Vec3 Scale);
HMM_Mat4 HMM_InvScale(HMM_Mat4 ScaleMatrix);
HMM_Mat4 _HMM_LookAt(HMM_Vec3 F, HMM_Vec3 S, HMM_Vec3 U, HMM_Vec3 Eye);
HMM_Mat4 HMM_LookAt_RH(HMM_Vec3 Eye, HMM_Vec3 Center, HMM_Vec3 Up);
HMM_Mat4 HMM_LookAt_LH(HMM_Vec3 Eye, HMM_Vec3 Center, HMM_Vec3 Up);
HMM_Mat4 HMM_InvLookAt(HMM_Mat4 Matrix);
/*
* Quaternion operations
*/
HMM_Vec3 HMM_QVRot(HMM_Vec3 v, HMM_Quat q)
;
HMM_Quat HMM_Q(float X, float Y, float Z, float W);
HMM_Quat HMM_QV4(HMM_Vec4 Vector);
HMM_Quat HMM_AddQ(HMM_Quat Left, HMM_Quat Right);
HMM_Quat HMM_SubQ(HMM_Quat Left, HMM_Quat Right);
HMM_Quat HMM_MulQ(HMM_Quat Left, HMM_Quat Right);
HMM_Quat HMM_MulQF(HMM_Quat Left, float Multiplicative);
HMM_Quat HMM_DivQF(HMM_Quat Left, float Divnd);
float HMM_DotQ(HMM_Quat Left, HMM_Quat Right);
HMM_Quat HMM_InvQ(HMM_Quat Left);
HMM_Quat HMM_NormQ(HMM_Quat Quat);
HMM_Quat _HMM_MixQ(HMM_Quat Left, float MixLeft, HMM_Quat Right, float MixRight);
HMM_Quat HMM_NLerp(HMM_Quat Left, float Time, HMM_Quat Right);
HMM_Quat HMM_SLerp(HMM_Quat Left, float Time, HMM_Quat Right);
HMM_Mat4 HMM_QToM4(HMM_Quat Left);
HMM_Mat4 HMM_M4TRS(HMM_Vec3 t, HMM_Quat q, HMM_Vec3 s);
HMM_Mat3 HMM_M3TRS(HMM_Vec2 t, float angle, HMM_Vec2 s);
HMM_Mat3 HMM_Mat4ToMat3(HMM_Mat4 m);
HMM_Quat HMM_M4ToQ_RH(HMM_Mat4 M);
HMM_Quat HMM_M4ToQ_LH(HMM_Mat4 M);
HMM_Quat HMM_QFromAxisAngle_RH(HMM_Vec3 Axis, float AngleOfRotation);
HMM_Quat HMM_QFromAxisAngle_LH(HMM_Vec3 Axis, float AngleOfRotation);
float HMM_Q_Roll(HMM_Quat q);
float HMM_Q_Yaw(HMM_Quat q);
float HMM_Q_Pitch(HMM_Quat q);
#endif /* HANDMADE_MATH_H */

2
Makefile Normal file
View File

@@ -0,0 +1,2 @@
default:
cc -fPIC -shared *.c -I../cell/source -Iimgui -lSDL3 -lcell -o prosperon.dylib

352
action.cm Normal file
View File

@@ -0,0 +1,352 @@
// Action mapping system that sits at the root of the scene tree
// Consumes raw input and reissues as named actions
var io = use('cellfs')
var input = use('input')
var json = use('json')
var action = {}
var controller_map = {
ps3: 'playstation',
ps4: 'playstation',
ps5: 'playstation',
xbox: 'xbox360',
xbox: 'xboxone',
switch: 'switchpro',
xbox: 'standard',
gamecube: 'gamecube',
switch: 'joyconleft',
switch: 'joyconright',
switch: 'joyconpair'
}
action.get_icon_for_action = function(action)
{
var bindings = this.get_bindings_for_device(action)
if (!length(bindings)) return null
var primary_binding = bindings[0]
var button = null
var key_mapping = {
'escape': 'escape',
'return': 'return',
'space': 'space',
'up': 'arrow_up',
'down': 'arrow_down',
'left': 'arrow_left',
'right': 'arrow_right'
}
var key = null
var controller_prefix = null
var gamepad_mapping = null
var icon_name = null
if (this.current_device == 'keyboard') {
if (starts_with(primary_binding, 'mouse_button_')) {
button = replace(primary_binding, 'mouse_button_', '')
return 'ui/mouse/mouse_' + button + '.png'
} else {
// Handle special keyboard keys
key = key_mapping[primary_binding] || primary_binding
return 'ui/keyboard/keyboard_' + key + '.png'
}
}
if (this.current_device == 'gamepad' && this.current_gamepad_type) {
controller_prefix = controller_map[this.current_gamepad_type] || 'playstation'
// Map gamepad inputs to icon names
gamepad_mapping = {
'gamepad_a': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_cross' : 'xbox_button_a',
'gamepad_b': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_circle' : 'xbox_button_b',
'gamepad_x': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_square' : 'xbox_button_x',
'gamepad_y': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_triangle' : 'xbox_button_y',
'gamepad_dpup': controller_prefix + '_dpad_up',
'gamepad_dpdown': controller_prefix + '_dpad_down',
'gamepad_dpleft': controller_prefix + '_dpad_left',
'gamepad_dpright': controller_prefix + '_dpad_right',
'gamepad_l1': controller_prefix + '_trigger_l1',
'gamepad_r1': controller_prefix + '_trigger_r1',
'gamepad_l2': controller_prefix + '_trigger_l2',
'gamepad_r2': controller_prefix + '_trigger_r2',
'gamepad_start': this.get_start_button_icon()
}
icon_name = gamepad_mapping[primary_binding]
if (icon_name) {
return 'ui/' + controller_prefix + '/' + icon_name + '.png'
}
}
return null
}
action.get_start_button_icon = function() {
if (this.current_gamepad_type == 'ps3') {
return 'playstation3_button_start'
} else if (this.current_gamepad_type == 'ps4') {
return 'playstation4_button_options'
} else if (this.current_gamepad_type == 'ps5') {
return 'playstation5_button_options'
} else {
return 'xbox_button_start'
}
}
var default_action_map = {
'move_up': ['w', 'gamepad_dpup', 'swipe_up'],
'move_left': ['a', 'gamepad_dpleft', 'swipe_left'],
'move_down': ['s', 'gamepad_dpdown', 'swipe_down'],
'move_right': ['d', 'gamepad_dpright', 'swipe_right'],
'accio': ['space', 'gamepad_x'],
'reset': ['r', 'gamepad_y'],
'undo': ['z'],
'menu': ['escape', 'gamepad_start'],
'ui_up': ['w', 'up', 'gamepad_dpup'],
'ui_down': ['s', 'down', 'gamepad_dpdown'],
'ui_left': ['a', 'left', 'gamepad_dpleft'],
'ui_right': ['d', 'right', 'gamepad_dpright'],
'confirm': ['return', 'space', 'mouse_button_left', 'gamepad_b'],
'cancel': ['escape', 'gamepad_a'],
// Editor controls
'brush_left': ['gamepad_dpleft', 'left'],
'brush_right': ['gamepad_dpright', 'right'],
'tool_up': ['gamepad_dpup', 'up'],
'tool_down': ['gamepad_dpdown', 'down'],
'tool_use': ['gamepad_a', 'mouse_button_left'],
'editor_undo': [',', 'gamepad_l1'],
'editor_redo': ['.', 'gamepad_r1'],
'editor_inspect': ['C-p'],
'editor_marquee': ['mouse_button_right', 'gamepad_r2'],
'editor_erase': ['x', 'gamepad_b'],
'editor_eyedrop': ['i', 'gamepad_x'],
'editor_grab': ['mouse_button_middle', 'gamepad_y'],
'editor_ccw': ['gamepad_l1', 'q'],
'editor_cw': ['gamepad_r1', 'e'],
}
default_action_map.witch_up = 'up'
default_action_map.witch_down = 'down'
default_action_map.witch_left = 'left'
default_action_map.witch_right = 'right'
default_action_map.accion = 'y'
// Utility to detect device from input id
function detect_device(input_id) {
if (starts_with(input_id, 'gamepad_')) return 'gamepad'
if (starts_with(input_id, 'swipe_')) return 'touch'
if (starts_with(input_id, 'mouse_button')) return 'keyboard' // Mouse buttons are part of keyboard/mouse controller
if (starts_with(input_id, 'touch_')) return 'touch'
return 'keyboard'
}
// Display names for actions
var action_display_names = {
'move_up': 'Move Up',
'move_down': 'Move Down',
'move_left': 'Move Left',
'move_right': 'Move Right',
'ui_up': 'UI Up',
'ui_down': 'UI Down',
'ui_left': 'UI Left',
'ui_right': 'UI Right',
'menu': 'Menu',
'confirm': 'Confirm',
'cancel': 'Cancel',
'reset': 'Reset',
'accio': 'Summon',
'brush_left': 'Previous Entity',
'brush_right': 'Next Entity',
'tool_up': 'Previous Tool',
'tool_down': 'Next Tool',
'tool_use': 'Use Tool'
}
action.down = {}
action.action_map = {}
action.display_names = action_display_names
action.is_rebinding = false
action.rebind_target = null
action.current_device = 'keyboard'
action.current_gamepad_type = null
// Copy defaults
arrfor(array(default_action_map), function(key) {
action.action_map[key] = array(default_action_map[key])
})
// Swiperecognizer state & tuning
var swipe = { x0:null, y0:null, t0:0 }
var SWIPE_MIN_DIST = 30 // pixels
var SWIPE_MAX_TIME = 500 // ms
action.on_input = function(action_id, evt)
{
// 1) Detect & store which device user is on (only from raw input, ignore passive inputs)
var new_device = null
var is_real_kb_mouse = false
var gamepad_type = null
if (action_id != 'mouse_move' && !starts_with(action_id, 'mouse_pos') && evt.pressed) {
new_device = detect_device(action_id)
// For keyboard/mouse detection, also check if the event has modifier keys or is a mouse button
// This helps distinguish real keyboard/mouse events from mapped actions
is_real_kb_mouse = (new_device == 'keyboard' &&
(evt.ctrl != null || evt.shift != null || evt.alt != null ||
starts_with(action_id, 'mouse_button')))
if (new_device == 'keyboard' && !is_real_kb_mouse) {
// This might be a mapped action, not a real keyboard/mouse event
return
}
if (new_device != this.current_device) {
if (new_device == 'gamepad') {
gamepad_type = evt.which != null ? input.gamepad_id_to_type(evt.which) : 'unknown'
log.console("Input switched to 'gamepad' (event: " + action_id + ", type: " + gamepad_type + ")")
this.current_gamepad_type = gamepad_type
} else if (this.current_device == 'gamepad') {
log.console("Input switched to 'keyboard/mouse' (event: " + action_id + ")")
}
this.current_device = new_device
}
}
// 2) If we're in rebind mode, grab the raw input
if (this.is_rebinding) {
if (evt.pressed) {
// Only bind if it's from the same device type
if (detect_device(action_id) == this.current_device) {
this.rebind_action(this.rebind_target, action_id)
this.save_bindings()
}
// Exit rebind mode
this.is_rebinding = false
this.rebind_target = null
}
return // Don't also fire the mapped action
}
// 3) Otherwise, find all mapped actions for this input
var matched_actions = []
arrfor(array(this.action_map), mapped_action => {
if (find(this.action_map[mapped_action], action_id) != null) {
matched_actions[] = mapped_action
if (evt.pressed)
this.down[mapped_action] = true
else if (evt.released)
this.down[mapped_action] = false
}
})
// Send all matched actions (only if we found mappings - this means it's a raw input)
var i = 0
if (length(matched_actions) > 0) {
for (i = 0; i < length(matched_actions); i++) {
// scene.recurse(game.root, 'on_input', [matched_actions[i], evt])
}
}
}
action.start_rebind = function(action_name) {
if (!this.action_map[action_name]) return
this.is_rebinding = true
this.rebind_target = action_name
}
action.rebind_action = function(action_name, new_key) {
if (!this.action_map[action_name]) return
// Remove this key from all other actions
arrfor(array(this.action_map), act => {
var idx = find(this.action_map[act], new_key)
if (idx != null)
this.action_map[act] = array(array(this.action_map[act], 0, idx), array(this.action_map[act], idx+1))
})
// Clear existing bindings for the current device from the target action
var target_bindings = this.action_map[action_name]
var i = length(target_bindings) - 1
for (; i >= 0; i--) {
if (detect_device(target_bindings[i]) == this.current_device)
this.action_map[action_name] = array(array(this.action_map[action_name], 0, i), array(this.action_map[action_name], i+1))
}
// Only insert into the target if it's the right device
if (detect_device(new_key) == this.current_device)
this.action_map[action_name].unshift(new_key)
}
// Returns bindings for the current device only
action.get_bindings_for_device = function(action_name) {
var all = this.action_map[action_name] || []
var self = this
return filter(all, function(id) { return detect_device(id) == self.current_device })
}
// Returns the primary binding for display - prefer keyboard/mouse, then others
action.get_primary_binding = function(action_name) {
var all = this.action_map[action_name] || []
if (!length(all)) return '(unbound)'
// Prefer keyboard/mouse bindings for display stability (mouse buttons now detect as keyboard)
var keyboard = filter(all, function(id) { return detect_device(id) == 'keyboard' })
if (length(keyboard)) return keyboard[0]
// Fall back to any binding
return all[0]
}
// Returns the binding for the current device, or "Unbound!" if none
action.get_current_device_binding = function(action_name) {
var device_bindings = this.get_bindings_for_device(action_name)
if (!length(device_bindings)) return 'Unbound!'
return device_bindings[0]
}
action.save_bindings = function() {
var self = this
var _save = function() {
io.slurpwrite('keybindings.json', json.encode(self.action_map))
} disruption {
log.console("Failed to save key bindings")
}
_save()
}
action.load_bindings = function() {
var self = this
var _load = function() {
var data = null
var bindings = null
if (io.exists('keybindings.json')) {
data = io.slurp('keybindings.json')
bindings = object(json.decode(data), self.action_map)
}
} disruption {
log.console("Failed to load key bindings")
}
_load()
}
action.reset_to_defaults = function() {
this.action_map = object(default_action_map)
this.save_bindings()
}
return function()
{
var obj = meme(action)
obj.action_map = object(default_action_map)
obj.display_names = action_display_names
obj.is_rebinding = false
obj.rebind_target = null
obj.current_device = 'keyboard'
obj.current_gamepad_type = null
obj.load_bindings()
return obj
}

95
anim2d.cm Normal file
View File

@@ -0,0 +1,95 @@
var film2d = use('film2d')
var graphics = use('graphics')
var anim_proto = {
type: 'sprite',
play: function(name) {
var anim = name ? this._anims[name] : this._anims
if (!anim || !anim.frames) return this
this._anim = anim
this._frame = 0
this._elapsed = 0
this._playing = true
this.image = anim.frames[0].image
return this
},
stop: function() {
this._playing = false
return this
},
resume: function() {
this._playing = true
return this
},
update: function(dt) {
if (!this._playing || !this._anim) return
var frames = this._anim.frames
this._elapsed += dt
while (this._elapsed >= frames[this._frame].time) {
this._elapsed -= frames[this._frame].time
this._frame++
if (this._frame >= length(frames)) {
if (this._anim.loop) {
this._frame = 0
} else {
this._frame = length(frames) - 1
this._playing = false
break
}
}
}
this.image = frames[this._frame].image
},
destroy: function() {
film2d.unregister(this._id)
}
}
return function(props) {
var img = props.image || props.anim
var anims = graphics.texture(img)
var defaults = {
type: 'sprite',
pos: {x: 0, y: 0},
width: null,
height: null,
anchor_x: 0.5,
anchor_y: 0.5,
rotation: 0,
color: {r: 1, g: 1, b: 1, a: 1},
opacity: 1,
tint: {r: 1, g: 1, b: 1, a: 1},
filter: 'nearest',
plane: 'default',
layer: 0,
groups: [],
visible: true
}
var data = object(defaults, props)
data._anims = anims
data._anim = null
data._frame = 0
data._elapsed = 0
data._playing = false
data.image = null
var s = meme(anim_proto, data)
// Auto-play: if anims is a single animation, start it
if (anims && anims.frames) {
s._anim = anims
s._frame = 0
s._playing = true
s.image = anims.frames[0].image
}
film2d.register(s)
return s
}

226
camera.cm
View File

@@ -1,121 +1,153 @@
var backend = use('sdl_gpu')
var cam = {}
/*
presentation can be one of
/*
presentation can be one of
letterbox
overscan
stretch
... or simply 'null' for no presentation
.. or simply 'null' for no presentation
*/
function rect_contains_pt(rect, x, y) {
return x >= rect.x && x <= rect.x + rect.width &&
y >= rect.y && y <= rect.y + rect.height
}
function place_rect(src, dst, mode) {
if (mode == 'stretch')
return {x: 0, y: 0, width: dst.width, height: dst.height}
var sx = 0, sy = 0, s = 0, w = 0, h = 0, scale = 0
if (mode == 'integer_scale') {
sx = floor(dst.width / src.width)
sy = floor(dst.height / src.height)
s = max(1, min(sx, sy))
w = src.width * s
h = src.height * s
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
// letterbox (default)
scale = min(dst.width / src.width, dst.height / src.height)
w = src.width * scale
h = src.height * scale
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
var basecam = {
pos: [0,0], // where it is
pos: {x:0,y:0},
ortho:true,
width: 100,
aspect_ratio: 16/9,
width: 1,
height: 1,
fov:50,
near_z:0,
far_z:1000,
anchor:[0.5,0.5],
anchor: {x:0.5,y:0.5},
rotation:[0,0,0,1],
presentation: "letterbox",
background: {r:1,g:1,b:1,a:0},
viewport: {x:0,y:0,width:1,height:1},
}
aspect_ratio() {
return this.y/this.x;
},
basecam.draw_rect = function(size)
{
var mode = this.presentation || "letterbox"
var vp = {
x:this.viewport.x,
y:1-this.viewport.y-this.viewport.height,
width:this.viewport.width,
height:this.viewport.height
}
var src_rect = {x:0,y:0,width:this.size.x,height:this.size.y}
var dst_rect = {x:vp.x*size.x,y:vp.y*size.y,width:vp.width*size.x,height:vp.height*size.y};
return mode_rect(src_rect,dst_rect,mode);
}
rect() {
return {x:0, y:0, width: this.width, height: this.height}
},
basecam.screen2camera = function(pos)
{
var draw_rect = this.draw_rect(prosperon.window.size);
var ret = [pos.x-draw_rect.x, pos.y - draw_rect.y];
ret.x /= draw_rect.width;
ret.y /= draw_rect.height;
ret.y = 1 - ret.y;
return ret;
}
basecam.screen2hud = function(pos)
{
var cam = this.screen2camera(pos);
cam.x *= this.size.x;
cam.y *= this.size.y;
return cam;
}
basecam.screen2world = function(pos)
{
var hud = this.screen2hud(pos);
hud.x += this.transform.pos.x - this.size.x/2;
hud.y += this.transform.pos.y - this.size.y/2;
return hud;
}
function mode_rect(src,dst,mode = "stretch")
{
var aspect_src = src.width/src.height;
var aspect_dst = dst.width/dst.height;
var out = {
x:dst.x,
y:dst.y,
width:dst.width,
height:dst.height
};
if (mode == "stretch") return out;
if (mode == "letterbox") {
if (aspect_src > aspect_dst) {
var scaled_h = out.width/aspect_src;
var off = (out.height - scaled_h) * 0.5;
out.y += off;
out.height = scaled_h;
} else {
var scaled_w =out.height * aspect_src;
var off = (out.width - scaled_w) * 0.5;
out.x += off;
out.width = scaled_w;
sensor() {
var ws = backend.get_window_size()
return {
width: ws.width,
height: ws.height,
}
} else if (mode == "overscan"){
if (aspect_src > aspect_dst) {
var scaled_w = out.height * aspect_src;
var off = (out.width - scaled_w) * 0.5;
out.x += off;
out.width = scaled_w;
} else {
var scaled_h = out.width / aspect_src;
var off = (out.height - scaled_h) * 0.5;
out.y += off;
out.height = scaled_h;
}
}
return out;
},
// The rectangle (in window pixels) where this cameras target will be drawn
screen_rect() {
var src = { width: this.width, height: this.height }
var ws = backend.get_window_size()
var dst = { x: 0, y: 0, width: ws.width, height: ws.height }
return place_rect(src, dst, this.presentation)
},
// --- Space converters ---------------------------------------------------
// World -> View UV [0..1] independent of pixels/letterbox
world_to_view_uv(wx, wy) {
var ax = this.anchor.x, ay = this.anchor.y
var u = (wx - this.pos.x) / this.width + ax
var v = (wy - this.pos.y) / this.height + ay
// apply viewport (maps camera-local UV into sub-rect)
var vp = this.viewport
u = vp.x + u * vp.width
v = vp.y + v * vp.height
return { x: u, y: v }
},
// View UV -> World
view_uv_to_world(u, v) {
var vp = this.viewport
var uu = (u - vp.x) / vp.width
var vv = (v - vp.y) / vp.height
var ax = this.anchor.x, ay = this.anchor.y
var wx = this.pos.x + (uu - ax) * this.width
var wy = this.pos.y + (vv - ay) * this.height
return { x: wx, y: wy }
},
// World -> Window pixels (what you want for hit-tests)
world_to_window(wx, wy) {
var uv = this.world_to_view_uv(wx, wy)
var sr = this.screen_rect()
var sx = sr.x + uv.x * sr.width
var sy = sr.y + uv.y * sr.height
return { x: sx, y: sy }
},
// Window pixels -> World (mouse picking)
window_to_world(sx, sy) {
var sr = this.screen_rect()
var u = (sx - sr.x) / sr.width
var v = 1 - (sy - sr.y) / sr.height
return this.view_uv_to_world(u, v)
},
// World -> normalized window [0..1]
world_to_screen_norm(wx, wy) {
var p = this.world_to_window(wx, wy)
var ws = backend.get_window_size()
return { x: p.x / ws.width, y: p.y / ws.height }
},
// Normalized window [0..1] -> World
screen_norm_to_world(nx, ny) {
var ws = backend.get_window_size()
var sx = nx * ws.width
var sy = ny * ws.height
return this.window_to_world(sx, sy)
},
screen_to_world: function(sx, sy) {
// sx, sy are normalized screen coordinates (0-1), bottom left [0,0], top right [1,1]
var screen_rect = this.screen_rect()
var ws = backend.get_window_size()
var pixel_x = sx * ws.width
var pixel_y = sy * ws.height
var rel_x = (pixel_x - screen_rect.x) / screen_rect.width
var rel_y = (pixel_y - screen_rect.y) / screen_rect.height
var ax = this.anchor.x
var ay = this.anchor.y
var world_x = this.pos.x + (rel_x - ax) * this.width
var world_y = this.pos.y + (rel_y - ay) * this.height
return {x: world_x, y: world_y}
},
}
cam.make = function()
{
var c = Object.create(basecam)
c.transform = new transform
c.transform.unit()
c.size = [640,360]
c.mode = 'keep'
c.viewport = {x:0,y:0,width:1,height:1}
c.fov = 45
c.type = 'ortho'
c.ortho = true
c.aspect = 16/9
return c
cam.make = function(config) {
return meme(basecam, config || {})
}
return cam

51
cbuf.h Normal file
View File

@@ -0,0 +1,51 @@
#ifndef CIRCBUF_H
#define CIRCBUF_H
#include <stdlib.h>
#include <stdint.h>
static inline unsigned int powof2(unsigned int x)
{
x = x-1;
x |= (x >> 1);
x |= (x >> 2);
x |= (x >> 4);
x |= (x >> 8);
x |= (x >> 16);
return x+1;
}
struct rheader
{
unsigned int read;
unsigned int write;
int len;
};
#define ringheader(r) ((struct rheader *)r-1)
static inline void *ringmake(void *ring, size_t elemsize, unsigned int n)
{
n = powof2(n);
if (ring) {
struct rheader *h = ringheader(ring);
if (n <= h->len) return h+1;
h = realloc(h, elemsize*n+sizeof(struct rheader));
return h+1;
}
struct rheader *h = malloc(elemsize*n+sizeof(struct rheader));
h->len = n; h->read = 0; h->write = 0;
return h+1;
}
#define ringnew(r,n) (r = ringmake(r, sizeof(*r),n))
#define ringfree(r) ((r) ? free(ringheader(r)) : 0)
#define ringmask(r,v) (v & (ringheader(r)->len-1))
#define ringpush(r,v) (r[ringmask(r,ringheader(r)->write++)] = v)
#define ringshift(r) (r[ringmask(r,ringheader(r)->read++)])
#define ringsize(r) ((r) ? ringheader(r)->write - ringheader(r)->read : 0)
#define ringfull(r) ((r) ? ringsize(r) == ringheader(r)->len : 0)
#define ringempty(r) ((r) ? ringheader(r)->read == ringheader(r)->write : 0)
#endif

12
cell.toml Normal file
View File

@@ -0,0 +1,12 @@
[compilation]
LDFLAGS = "-lSDL3 -lstdc++ -lm -lc++"
CFLAGS = "-Isrc/imgui"
[dependencies]
rtree = "gitea.pockle.world/john/cell-rtree"
dmon = "gitea.pockle.world/john/cell-watch"
libsamplerate = "gitea.pockle.world/john/cell-libsamplerate"
sdl3 = "gitea.pockle.world/john/cell-sdl3"
image = "gitea.pockle.world/john/cell-image"
soundwave = "gitea.pockle.world/john/soundwave"

784
clay.cm
View File

@@ -1,410 +1,440 @@
// Layout code
// Contain is for how it will treat its children. If they should be laid out as a row, or column, or in a flex style, etc.
// clay.cm - UI layout engine emitting flat drawables with annotated tree
//
// - Uses meme/merge for config chains
// - Emits flat list of drawables for film2d
// - Supports scissor clipping
// - Returns annotated tree root with .drawables for clay_input compatibility
var layout = use('layout')
var geometry = use('geometry')
var draw = use('prosperon/draw2d')
var graphics = use('graphics')
var util = use('util')
var input = use('input')
var prosperon = use('prosperon')
var CHILDREN = Symbol('children')
var PARENT = Symbol('parent')
var clay = {}
function normalizeSpacing(spacing) {
if (typeof spacing == 'number') {
return {l: spacing, r: spacing, t: spacing, b: spacing}
} else if (Array.isArray(spacing)) {
if (spacing.length == 2) {
return {l: spacing[0], r: spacing[0], t: spacing[1], b: spacing[1]}
} else if (spacing.length == 4) {
return {l: spacing[0], r: spacing[1], t: spacing[2], b: spacing[3]}
}
} else if (typeof spacing == 'object') {
return {l: spacing.l || 0, r: spacing.r || 0, t: spacing.t || 0, b: spacing.b || 0}
} else {
return {l:0, r:0, t:0, b:0}
}
}
// Unique key objects for tree traversal (used by clay_input)
var CHILDREN = {}
var PARENT = {}
clay.CHILDREN = CHILDREN
clay.PARENT = PARENT
var lay_ctx = layout.make_context();
// Layout context
var lay_ctx = layout.make_context()
var clay_base = {
// Base configuration for UI elements
var base_config = {
font: null,
background_image: null,
slice: 0,
font: 'smalle.16',
font_size: null,
color: {r:1,g:1,b:1,a:1},
spacing:0,
padding:0,
margin:0,
offset:{x:0, y:0},
size:null,
font_path: 'fonts/dos', // Default font
font_size: 16,
color: {r:1, g:1, b:1, a:1},
spacing: 0,
padding: 0,
margin: 0,
offset: {x:0, y:0},
size: null,
background_color: null,
clipped: false,
text_break: 'word',
text_align: 'left',
max_size: null, // {width: null, height: null}
};
var root_item;
var root_config;
var tree_root;
var clay = {}
clay.CHILDREN = CHILDREN
clay.PARENT = PARENT
var focused_textbox = null
clay.behave = layout.behave;
clay.contain = layout.contain;
clay.draw = function draw(fn, size = [prosperon.camera.width, prosperon.camera.height])
{
lay_ctx.reset();
var root = lay_ctx.item();
// Accept both array and object formats
if (Array.isArray(size)) {
size = {width: size[0], height: size[1]};
}
lay_ctx.set_size(root,size);
lay_ctx.set_contain(root,layout.contain.row);
root_item = root;
root_config = Object.assign({}, clay_base);
tree_root = {
id: root,
config: root_config,
};
tree_root[CHILDREN] = [];
tree_root[PARENT] = null;
fn()
lay_ctx.run();
// Adjust bounding boxes for padding - traverse tree instead of array
function adjust_bounding_boxes(node) {
node.content = lay_ctx.get_rect(node.id);
node.boundingbox = Object.assign({}, node.content);
var padding = normalizeSpacing(node.config.padding || 0);
node.boundingbox.x -= padding.l;
node.boundingbox.y -= padding.t;
node.boundingbox.width += padding.l + padding.r;
node.boundingbox.height += padding.t + padding.b;
node.marginbox = Object.assign({}, node.content);
var margin = normalizeSpacing(node.config.margin || 0);
node.marginbox.x -= margin.l;
node.marginbox.y -= margin.t;
node.marginbox.width += margin.l+margin.r;
node.marginbox.height += margin.t+margin.b;
// Apply max_size clamping post-layout
if (node.config.max_size) {
if (node.config.max_size.width != null) {
// Clamp the layout rect size
var rect = lay_ctx.get_rect(node.id);
rect.width = Math.min(rect.width, node.config.max_size.width);
// Also clamp bounding box
node.content.width = Math.min(node.content.width, node.config.max_size.width);
node.boundingbox.width = Math.min(node.boundingbox.width, node.config.max_size.width + padding.l + padding.r);
node.marginbox.width = Math.min(node.marginbox.width, node.config.max_size.width + padding.l + padding.r + margin.l + margin.r);
}
if (node.config.max_size.height != null) {
// Clamp the layout rect size
var rect = lay_ctx.get_rect(node.id);
rect.height = Math.min(rect.height, node.config.max_size.height);
// Also clamp bounding box
node.content.height = Math.min(node.content.height, node.config.max_size.height);
node.boundingbox.height = Math.min(node.boundingbox.height, node.config.max_size.height + padding.t + padding.b);
node.marginbox.height = Math.min(node.marginbox.height, node.config.max_size.height + padding.t + padding.b + margin.t + margin.b);
}
}
node.content.y *= -1;
node.content.y += size.height;
node.boundingbox.y *= -1;
node.boundingbox.y += size.height;
node.content.anchor_y = 1;
node.boundingbox.anchor_y = 1;
// Recursively adjust children
if (node[CHILDREN]) {
for (var child of node[CHILDREN]) {
adjust_bounding_boxes(child);
}
}
}
adjust_bounding_boxes(tree_root);
return tree_root;
max_size: null,
contain: 0,
behave: 0
}
function create_view_fn(base_config)
{
var base = Object.assign(Object.create(clay_base), base_config);
return function view(config = {}, fn) {
config.__proto__ = base;
var item = add_item(config);
function normalize_color(c, fallback) {
var fb = fallback || {r:1, g:1, b:1, a:1}
if (!c) return {r:fb.r, g:fb.g, b:fb.b, a:fb.a}
return {
r: c.r != null ? c.r : fb.r,
g: c.g != null ? c.g : fb.g,
b: c.b != null ? c.b : fb.b,
a: c.a != null ? c.a : fb.a
}
}
var prev_item = root_item;
var prev_config = root_config;
var prev_tree_root = tree_root;
root_item = item;
root_config = config;
root_config._childIndex = 0; // Initialize child index
// Find the tree node for this item and set it as current tree_root
if (prev_tree_root[CHILDREN] && prev_tree_root[CHILDREN].length > 0) {
tree_root = prev_tree_root[CHILDREN][prev_tree_root[CHILDREN].length - 1];
function normalize_spacing(s) {
if (is_number(s)) return {l:s, r:s, t:s, b:s}
if (is_array(s)) {
if (length(s) == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
if (length(s) == 4) return {l:s[0], r:s[1], t:s[2], b:s[3]}
}
if (is_object(s)) return {l:s.l||0, r:s.r||0, t:s.t||0, b:s.b||0}
return {l:0, r:0, t:0, b:0}
}
// Tree building state
var root_item = null
var tree_root = null
var config_stack = []
// Rewriting state management for cleaner recursion
var tree_stack = []
var _next_id = 0
function annotate_tree(node, root_height, parent_node) {
var rect = lay_ctx.get_rect(node.id)
node.boundingbox = {
x: rect.x,
y: root_height - (rect.y + rect.height),
width: rect.width,
height: rect.height
}
node[CHILDREN] = node.children
node[PARENT] = parent_node
arrfor(node.children, function(child) {
annotate_tree(child, root_height, node)
})
}
clay.layout = function(fn, size) {
var sz = is_array(size) ? {width: size[0], height: size[1]} : size
lay_ctx.reset()
_next_id = 0
var root_id = lay_ctx.item()
lay_ctx.set_size(root_id, sz)
lay_ctx.set_contain(root_id, layout.contain.row)
var root_node = {
id: root_id,
config: meme(base_config, {size: sz}),
children: []
}
tree_stack = [root_node]
fn() // User builds tree
lay_ctx.run()
// Annotate tree for clay_input (boundingbox, CHILDREN, PARENT)
annotate_tree(root_node, sz.height, null)
// Build flat drawable list and attach to tree root
root_node.drawables = build_drawables(root_node, sz.height)
return root_node
}
function build_drawables(node, root_height, parent, parent_scissor) {
var p_abs_x = (parent && parent.x) || 0
var p_abs_y = (parent && parent.y) || 0
var p_layer = (parent && parent.layer) || 0
var rect = lay_ctx.get_rect(node.id)
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
var abs_y = root_height - (rect.y + rect.height)
var abs_x = rect.x
// IMPORTANT: The offset in config is applied VISUALLY.
var vis_x = abs_x + node.config.offset.x
var vis_y = abs_y + node.config.offset.y
var drawables = []
// Scissor
var current_scissor = parent_scissor
var sx = vis_x
var sy = vis_y
var sw = rect.width
var sh = rect.height
var clip_right = null
var clip_bottom = null
if (node.config.clipped) {
// Intersect with parent
if (parent_scissor) {
sx = max(sx, parent_scissor.x)
sy = max(sy, parent_scissor.y)
clip_right = min(vis_x + sw, parent_scissor.x + parent_scissor.width)
clip_bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height)
sw = max(0, clip_right - sx)
sh = max(0, clip_bottom - sy)
}
current_scissor = {x: sx, y: sy, width: sw, height: sh}
}
// Background
if (node.config.background_image) {
_next_id = _next_id + 1
if (node.config.slice) {
drawables[] = {
_id: _next_id,
type: 'sprite',
image: node.config.background_image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
slice: node.config.slice,
color: node.config.background_color || {r:1, g:1, b:1, a:1},
layer: p_layer - 0.1,
scissor: current_scissor
}
} else {
// If no children yet, this shouldn't happen, but handle gracefully
tree_root = prev_tree_root;
drawables[] = {
_id: _next_id,
type: 'sprite',
image: node.config.background_image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.background_color || {r:1, g:1, b:1, a:1},
layer: p_layer - 0.1,
scissor: current_scissor
}
}
fn?.();
root_item = prev_item;
root_config = prev_config;
tree_root = prev_tree_root;
} else if (node.config.background_color) {
_next_id = _next_id + 1
drawables[] = {
_id: _next_id,
type: 'rect',
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.background_color,
layer: p_layer - 0.1,
scissor: current_scissor
}
}
}
clay.vstack = create_view_fn({
contain: layout.contain.column | layout.contain.start,
});
clay.hstack = create_view_fn({
contain: layout.contain.row | layout.contain.start,
});
clay.spacer = create_view_fn({
behave: layout.behave.hfill | layout.behave.vfill
});
clay.frame = create_view_fn({});
function image_size(img)
{
return [img.width * (img.rect?.width || 1), img.height * (img.rect?.height || 1)];
}
function add_item(config)
{
// Normalize the child's margin
var margin = normalizeSpacing(config.margin || 0);
var padding = normalizeSpacing(config.padding || 0);
var childGap = root_config.child_gap || 0;
// Adjust for child_gap
root_config._childIndex ??= 0
if (root_config._childIndex > 0) {
var parentContain = root_config.contain || 0;
var directionMask = layout.contain.row | layout.contain.column;
var direction = parentContain & directionMask;
var isVStack = direction == layout.contain.column;
var isHStack = direction == layout.contain.row;
if (isVStack) {
margin.t += childGap;
} else if (isHStack) {
margin.l += childGap;
// Content (Image/Text)
if (node.config.image) {
_next_id = _next_id + 1
drawables[] = {
_id: _next_id,
type: 'sprite',
image: node.config.image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.color,
layer: p_layer,
scissor: current_scissor
}
}
var use_config = Object.create(config);
use_config.margin = {
t: margin.t+padding.t,
b: margin.b+padding.b,
r:margin.r+padding.r,
l:margin.l+padding.l
};
var item = lay_ctx.item();
lay_ctx.set_margins(item, use_config.margin);
use_config.size ??= {width:0, height:0}
// Convert array to object if needed
if (Array.isArray(use_config.size)) {
use_config.size = {width: use_config.size[0], height: use_config.size[1]};
if (node.config.text) {
_next_id = _next_id + 1
drawables[] = {
_id: _next_id,
type: 'text',
text: node.config.text,
font: node.config.font_path,
size: node.config.font_size,
color: node.config.color,
pos: {x: vis_x, y: vis_y + rect.height},
anchor_y: 1.0,
layer: p_layer,
scissor: current_scissor
}
}
// Apply max_size constraint - only clamp computed size, don't set explicit size pre-layout
if (use_config.max_size) {
// max_size should not force explicit sizing pre-layout - let children compute natural size,
// then clamp the container after layout. For now, just ensure we don't set size to max_size.
// Children
arrfor(node.children, function(child) {
drawables = array(drawables, build_drawables(child, root_height, {x: vis_x, y: vis_y, layer: p_layer + 0.01}, current_scissor))
})
return drawables
}
// --- Item Creation Helpers ---
function process_configs(configs) {
var cfg = meme(base_config, configs)
// Parse shorthand font string (e.g. 'blackcastle.64') into font_path and font_size
var font_parts = null
var parsed_size = null
if (cfg.font && is_text(cfg.font)) {
font_parts = array(cfg.font, '.')
if (length(font_parts) >= 2) {
parsed_size = number(font_parts[length(font_parts) - 1])
if (parsed_size) {
cfg.font_size = parsed_size
cfg.font_path = font_parts[0]
}
}
}
lay_ctx.set_size(item,use_config.size);
lay_ctx.set_contain(item,use_config.contain);
lay_ctx.set_behave(item,use_config.behave);
var tree_node = {
cfg.color = normalize_color(cfg.color, base_config.color)
if (cfg.background_color) cfg.background_color = normalize_color(cfg.background_color, {r:1,g:1,b:1,a:1})
if (!cfg.offset) cfg.offset = {x:0, y:0}
else cfg.offset = {x: cfg.offset.x || 0, y: cfg.offset.y || 0}
return cfg
}
function push_node(configs, contain_mode) {
var config = process_configs(configs)
if (contain_mode != null) config.contain = contain_mode
var item = lay_ctx.item()
// Apply layout props
lay_ctx.set_margins(item, normalize_spacing(config.margin))
lay_ctx.set_contain(item, config.contain)
lay_ctx.set_behave(item, config.behave)
var s = config.size
if (s) {
if (is_array(s)) s = {width: s[0], height: s[1]}
lay_ctx.set_size(item, s)
}
var node = {
id: item,
config: use_config,
};
tree_node[CHILDREN] = [];
tree_node[PARENT] = tree_root;
tree_root[CHILDREN].push(tree_node);
lay_ctx.insert(root_item,item);
// Increment the parent's child index
root_config._childIndex++;
return item;
}
function rectify_configs(config_array)
{
if (config_array.length == 0)
config_array = [{}];
for (var i = config_array.length-1; i > 0; i--)
config_array[i].__proto__ = config_array[i-1];
config_array[0].__proto__ = clay_base;
var cleanobj = Object.create(config_array[config_array.length-1]);
return cleanobj;
}
clay.image = function image(path, ...configs)
{
var config = rectify_configs(configs);
var image = graphics.texture(path);
config.image = path; // Store the path string, not the texture object
config.size ??= {width: image.width, height: image.height};
add_item(config);
}
clay.text = function text(str, ...configs)
{
var config = rectify_configs(configs);
config.size ??= [0,0]
config.font = graphics.get_font(config.font)
config.text = str
var tsize = config.font.text_size(str, 0, config.size[0], config.text_break, config.text_align);
tsize.x = Math.ceil(tsize.x)
tsize.y = Math.ceil(tsize.y)
config.size = config.size.map((x,i) => Math.max(x, tsize[i]));
add_item(config);
}
/*
For a given size,
the layout engine should "see" size + margin
but its interior content should "see" size - padding
hence, the layout box should be size-padding, with margin of margin+padding
*/
var button_base = Object.assign(Object.create(clay_base), {
padding:0,
hovered:{
}
});
clay.button = function button(str, action, config = {})
{
config.__proto__ = button_base;
config.font = graphics.get_font(config.font)
config.size = config.font.text_size(str, 0, 0, config.text_break, config.text_align)
add_item(config);
config.text = str;
config.action = action;
}
var point = use('point')
clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0})
{
function draw_node(node) {
var config = node.config
var boundingbox = geometry.rect_move(node.boundingbox,point.add(pos,config.offset))
var content = geometry.rect_move(node.content,point.add(pos, config.offset))
if (config.background_image)
if (config.slice)
draw.slice9(config.background_image, boundingbox, config.slice, config.background_color)
else
draw.image(config.background_image, boundingbox, 0, config.color)
else if (config.background_color)
draw.rectangle(boundingbox, null, {color:config.background_color})
if (config.text) {
var baseline_y = content.y + content.height - config.font.ascent
draw.text(config.text, {x: content.x, y: baseline_y}, config.font, config.color, content.width)
}
if (config.image)
draw.image(config.image, content, 0, config.color)
if (config.clipped) {
draw.scissor(content)
}
// Recursively draw children
if (node[CHILDREN]) {
for (var child of node[CHILDREN]) {
draw_node(child);
}
}
if (config.clipped)
draw.scissor(null)
}
draw_node(tree_root);
}
var dbg_colors = {};
clay.debug_colors = dbg_colors;
dbg_colors.content = [1,0,0,0.1];
dbg_colors.boundingbox = [0,1,0,0,0.1];
dbg_colors.margin = [0,0,1,0.1];
clay.draw_debug = function draw_debug(tree_root, pos = {x:0, y:0})
{
function draw_debug_node(node) {
var boundingbox = geometry.rect_move(node.boundingbox,pos);
var content = geometry.rect_move(node.content,pos);
draw.rectangle(content, null, {color:dbg_colors.content});
draw.rectangle(boundingbox, null, {color:dbg_colors.boundingbox});
// draw.rectangle(geometry.rect_move(node.marginbox,pos), dbg_colors.margin);
// Recursively draw debug for children
if (node[CHILDREN]) {
for (var child of node[CHILDREN]) {
draw_debug_node(child);
}
}
}
draw_debug_node(tree_root);
}
clay.print_tree = function print_tree(tree_root, indent = 0) {
var indent_str = ' '.repeat(indent)
var node_type = 'unknown'
if (tree_root.config.text) {
node_type = 'text'
} else if (tree_root.config.image) {
node_type = 'image'
} else if (tree_root.config.contain) {
if (tree_root.config.contain & layout.contain.column) {
node_type = 'vstack'
} else if (tree_root.config.contain & layout.contain.row) {
node_type = 'hstack'
} else {
node_type = 'container'
}
} else {
node_type = 'node'
config: config,
children: []
}
log.console(`${indent_str}${node_type} (id: ${tree_root.id})`)
// Add to parent
var parent = tree_stack[length(tree_stack)-1]
parent.children[] = node
lay_ctx.insert(parent.id, item)
if (tree_root[CHILDREN] && tree_root[CHILDREN].length > 0) {
log.console(`${indent_str} children: ${tree_root[CHILDREN].length}`)
for (var child of tree_root[CHILDREN]) {
print_tree(child, indent + 1)
}
} else {
log.console(`${indent_str} (no children)`)
}
tree_stack[] = node
return node
}
function pop_node() {
tree_stack[]
}
// Generic container
clay.container = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
push_node(_configs, null)
if (_fn) _fn()
pop_node()
}
// Stacks
clay.vstack = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.column
push_node(_configs, c)
if (_fn) _fn()
pop_node()
}
clay.hstack = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.row
push_node(_configs, c)
if (_fn) _fn()
pop_node()
}
clay.zstack = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.layout
push_node(_configs, c)
if (_fn) _fn()
pop_node()
}
// Leaf nodes
clay.image = function(path, configs, extra) {
var img = graphics.texture(path)
var c = [{image: path}]
var _configs = configs ? (is_array(configs) ? configs : [configs]) : []
if (extra) _configs = array(_configs, is_array(extra) ? extra : [extra])
var final_config = process_configs(_configs)
if (!final_config.size && !final_config.behave)
c[] = {size: {width: img.width, height: img.height}}
push_node(array(c, _configs), null)
pop_node()
}
clay.text = function(str, configs, extra) {
var c = [{text: str}]
var _configs = configs ? (is_array(configs) ? configs : [configs]) : []
if (extra) _configs = array(_configs, is_array(extra) ? extra : [extra])
var final_config = process_configs(_configs)
if (!final_config.size && !final_config.behave) {
c[] = {size: {width: 100, height: 20}}
}
push_node(array(c, _configs), null)
pop_node()
}
clay.rectangle = function(configs) {
var _configs = configs ? (is_array(configs) ? configs : [configs]) : [{}]
push_node(_configs, null)
pop_node()
}
clay.button = function(str, action, configs) {
var btn_config = [{
padding: 10,
background_color: {r:0.3, g:0.3, b:0.4, a:1}
}]
var _configs = is_array(configs) ? configs : [configs]
var merged = process_configs(_configs)
clay.zstack(array(btn_config, _configs), function() {
clay.text(str, {color: {r:1,g:1,b:1,a:1}, font_path: merged.font_path})
})
}
// Spacer — fills available space
clay.spacer = function(config) {
var cfg = config || {}
if (!cfg.behave) cfg.behave = layout.behave.hfill | layout.behave.vfill
push_node([cfg], null)
pop_node()
}
// Convenience draw wrapper — auto-detects call patterns:
// clay.draw(fn) — default 640x360
// clay.draw(fn, size_obj) — fn first, size object second
// clay.draw(size_array, fn) — size array first, fn second
clay.draw = function(arg1, arg2) {
var fn = null
var size = {width: 640, height: 360}
if (is_function(arg1)) {
fn = arg1
if (arg2) {
if (is_array(arg2)) size = {width: arg2[0], height: arg2[1]}
else if (is_object(arg2)) size = arg2
}
} else if (is_array(arg1)) {
size = {width: arg1[0], height: arg1[1]}
fn = arg2
}
return clay.layout(fn, size)
}
// Offset all drawables in an array by a position
clay.offset_drawables = function(drawables, offset) {
var result = []
var i = 0
var d = null
for (i = 0; i < length(drawables); i = i + 1) {
d = meme(drawables[i])
d.pos = {x: d.pos.x + offset.x, y: d.pos.y + offset.y}
result[] = d
}
return result
}
// Constants
clay.behave = layout.behave
clay.contain = layout.contain
return clay

View File

@@ -2,8 +2,7 @@
// Separates input concerns from layout/rendering
var geometry = use('geometry')
var point = use('point')
var clay = use('prosperon/clay')
var clay = use('clay')
var clay_input = {}
@@ -35,13 +34,18 @@ function find_path(node, path, pos) {
if (!pointer_enabled(node)) return null
if (!rect_contains(node, pos)) return null
var next_path = path.concat(node)
var next_path = array(path)
next_path[] = node
var i = 0
var child = null
var child_path = null
if (node[clay.CHILDREN] && !should_skip_children(node)) {
// Children drawn later should be tested first; reverse if your render order differs
for (var i = node[clay.CHILDREN].length - 1; i >= 0; i--) {
var child = node[clay.CHILDREN][i]
var child_path = find_path(child, next_path, pos)
i = length(node[clay.CHILDREN]) - 1
for (; i >= 0; i--) {
child = node[clay.CHILDREN][i]
child_path = find_path(child, next_path, pos)
if (child_path) return child_path
}
}
@@ -52,7 +56,7 @@ function find_path(node, path, pos) {
clay_input.deepest = function deepest(tree_root, pos) {
var path = find_path(tree_root, [], pos) || []
var deepest = path.length ? path[path.length - 1] : null
var deepest = length(path) ? path[length(path) - 1] : null
return deepest
}
@@ -66,7 +70,8 @@ clay_input.bubble = function bubble(deepest, prop) {
return null
}
clay_input.click = function click(tree_root, mousepos, button = 'left') {
clay_input.click = function click(tree_root, mousepos, button) {
var _button = button || 'left'
var deepest = clay_input.deepest(tree_root, mousepos)
var action_target = clay_input.bubble(deepest, 'action')
if (action_target && action_target.config.action) action_target.config.action()
@@ -75,9 +80,9 @@ clay_input.click = function click(tree_root, mousepos, button = 'left') {
clay_input.get_actionable = function get_actionable(tree_root) {
var actionable = []
function walk(node) {
if (node.config.action) actionable.push(node)
if (node.config.action) actionable[] = node
if (node[clay.CHILDREN])
for (var child of node[clay.CHILDREN]) walk(child)
arrfor(node[clay.CHILDREN], walk)
}
walk(tree_root)
return actionable
@@ -86,9 +91,9 @@ clay_input.get_actionable = function get_actionable(tree_root) {
clay_input.filter = function filter(tree_root, predicate) {
var results = []
function rec(node) {
if (predicate(node)) results.push(node)
if (predicate(node)) results[] = node
if (node[clay.CHILDREN])
for (var child of node[clay.CHILDREN]) rec(child)
arrfor(node[clay.CHILDREN], rec)
}
rec(tree_root)
return results
@@ -98,10 +103,10 @@ clay_input.find_by_id = function find_by_id(tree_root, id) {
function rec(node) {
if (node.id == id) return node
if (node[clay.CHILDREN])
for (var child of node[clay.CHILDREN]) {
arrfor(node[clay.CHILDREN], function(child) {
var f = rec(child)
if (f) return f
}
})
return null
}
return rec(tree_root)

108
collision2d.cm Normal file
View File

@@ -0,0 +1,108 @@
var collision2d = {}
collision2d.body = function(config) {
var c = config || {}
return {
type: c.type || 'aabb',
width: c.width || 0,
height: c.height || 0,
radius: c.radius || 0,
offset: c.offset || {x: 0, y: 0},
layer: c.layer || 0,
mask: c.mask || 0xFFFF
}
}
function body_center(body, pos) {
return {
x: pos.x + body.offset.x,
y: pos.y + body.offset.y
}
}
function test_aabb_aabb(a, ap, b, bp) {
var ac = body_center(a, ap)
var bc = body_center(b, bp)
var ahw = a.width * 0.5
var ahh = a.height * 0.5
var bhw = b.width * 0.5
var bhh = b.height * 0.5
return abs(ac.x - bc.x) < ahw + bhw && abs(ac.y - bc.y) < ahh + bhh
}
function test_circle_circle(a, ap, b, bp) {
var ac = body_center(a, ap)
var bc = body_center(b, bp)
var dx = ac.x - bc.x
var dy = ac.y - bc.y
var r = a.radius + b.radius
return dx * dx + dy * dy < r * r
}
function test_aabb_circle(aabb, ap, circ, cp) {
var ac = body_center(aabb, ap)
var cc = body_center(circ, cp)
var hw = aabb.width * 0.5
var hh = aabb.height * 0.5
var cx = max(ac.x - hw, min(cc.x, ac.x + hw))
var cy = max(ac.y - hh, min(cc.y, ac.y + hh))
var dx = cc.x - cx
var dy = cc.y - cy
return dx * dx + dy * dy < circ.radius * circ.radius
}
collision2d.test = function(a, a_pos, b, b_pos) {
if (a.layer & b.mask == 0 || b.layer & a.mask == 0)
return false
if (a.type == 'aabb' && b.type == 'aabb')
return test_aabb_aabb(a, a_pos, b, b_pos)
if (a.type == 'circle' && b.type == 'circle')
return test_circle_circle(a, a_pos, b, b_pos)
if (a.type == 'aabb' && b.type == 'circle')
return test_aabb_circle(a, a_pos, b, b_pos)
if (a.type == 'circle' && b.type == 'aabb')
return test_aabb_circle(b, b_pos, a, a_pos)
return false
}
collision2d.overlap = function(body, pos, others) {
var results = []
var i = 0
var other = null
for (i = 0; i < length(others); i++) {
other = others[i]
if (collision2d.test(body, pos, other.body, other.pos))
results[] = other
}
return results
}
collision2d.overlap_point = function(point, bodies) {
var results = []
var i = 0
var b = null
var c = null
var hw = 0
var hh = 0
var dx = 0
var dy = 0
for (i = 0; i < length(bodies); i++) {
b = bodies[i]
c = body_center(b.body, b.pos)
if (b.body.type == 'aabb') {
hw = b.body.width * 0.5
hh = b.body.height * 0.5
if (abs(point.x - c.x) < hw && abs(point.y - c.y) < hh)
results[] = b
} else if (b.body.type == 'circle') {
dx = point.x - c.x
dy = point.y - c.y
if (dx * dx + dy * dy < b.body.radius * b.body.radius)
results[] = b
}
}
return results
}
return collision2d

View File

@@ -1,7 +1,7 @@
function tohex(n) {
var s = Math.floor(n).toString(16);
if (s.length == 1) s = "0" + s;
return s.toUpperCase();
var s = text(floor(n), 16)
if (length(s) == 1) s = "0" + s;
return upper(s);
};
var Color = {
@@ -22,19 +22,15 @@ Color.editor = {};
Color.editor.ur = Color.green;
Color.tohtml = function (v) {
var html = v.map(function (n) {
return tohex(n * 255);
});
return "#" + html.join("");
var html = array(v, n => tohex(n * 255));
return "#" + text(html);
};
var esc = {};
esc.reset = "\x1b[0";
esc.color = function (v) {
var c = v.map(function (n) {
return Math.floor(n * 255);
});
var truecolor = "\x1b[38;2;" + c.join(";") + ";";
var c = array(v, n => floor(n * 255));
var truecolor = "\x1b[38;2;" + text(c, ";") + ";";
return truecolor;
};
@@ -92,50 +88,46 @@ Color.Editor = {
/* Detects the format of all colors and munges them into a floating point format */
Color.normalize = function (c) {
var add_a = function (a) {
var n = this.slice();
var n = array(this);
n[3] = a;
return n;
};
for (var p of Object.keys(c)) {
if (typeof c[p] != "object") continue;
if (!Array.isArray(c[p])) {
arrfor(array(c), function(p) {
if (!is_object(c[p])) return;
if (!is_array(c[p])) {
Color.normalize(c[p]);
continue;
return;
}
// Add alpha channel if not present
if (c[p].length == 3) {
if (length(c[p]) == 3) {
c[p][3] = 1;
}
// Check if any values are > 1 (meaning they're in 0-255 format)
var needs_conversion = false;
for (var color of c[p]) {
arrfor(c[p], function(color) {
if (color > 1) {
needs_conversion = true;
break;
return;
}
}
})
// Convert from 0-255 to 0-1 if needed
if (needs_conversion) {
c[p] = c[p].map(function (x) {
return x / 255;
});
c[p] = array(c[p], x => x / 255);
}
c[p].alpha = add_a;
}
})
};
Color.normalize(Color);
var ColorMap = {};
ColorMap.makemap = function (map) {
var newmap = Object.create(ColorMap);
Object.assign(newmap, map);
return newmap;
return meme(ColorMap, map)
};
ColorMap.Jet = ColorMap.makemap({
0: [0, 0, 0.514],
@@ -189,22 +181,31 @@ ColorMap.Viridis = ColorMap.makemap({
Color.normalize(ColorMap);
ColorMap.sample = function (t, map = this) {
if (t < 0) return map[0];
if (t > 1) return map[1];
ColorMap.sample = function(t, map) {
var local_map = map || this
if (t < 0) return local_map[0]
if (t > 1) return local_map[1]
var lastkey = 0;
for (var key of Object.keys(map).sort()) {
var keys = sorted(array(local_map))
var lastkey = 0
var i = 0
var key = null
var b = null
var a = null
var tt = 0
for (i = 0; i < length(keys); i++) {
key = keys[i]
if (t < key) {
var b = map[key];
var a = map[lastkey];
var tt = (key - lastkey) * t;
return a.lerp(b, tt);
b = local_map[key]
a = local_map[lastkey]
tt = (t - lastkey) / (key - lastkey)
return a.lerp(b, tt)
}
lastkey = key;
lastkey = key
}
return map[1];
};
return local_map[1]
}
ColorMap.doc = {
sample: "Sample a given colormap at the given percentage (0 to 1).",

448
compositor.cm Normal file
View File

@@ -0,0 +1,448 @@
var effects_mod = use('effects')
var backend = use('sdl_gpu')
var film2d = use('film2d')
var compositor = {}
// Compile compositor config into render plan
compositor.compile = function(config) {
var ctx = {
passes: [],
targets: {},
counter: 0,
screen_size: backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720},
alloc: function(w, h, hint) {
var key = (hint || 't') + '_' + text(this.counter++)
this.targets[key] = {width: w, height: h, key: key}
return {type: 'target', key: key, width: w, height: h}
}
}
var group_effects = config.group_effects || {}
// Clear screen
if (config.clear)
ctx.passes[] = {type: 'clear', target: 'screen', color: config.clear}
// Process each plane (supports both 'planes' and legacy 'layers' key)
var planes = config.planes || config.layers || []
var i = 0
var plane = null
var type = null
for (i = 0; i < length(planes); i++) {
plane = planes[i]
type = plane.type || 'film2d'
if (type == 'imgui') {
compile_imgui_layer(plane, ctx)
} else {
compile_plane(plane, ctx, group_effects)
}
}
return {passes: ctx.passes, targets: ctx.targets, screen_size: ctx.screen_size}
}
function compile_imgui_layer(layer, ctx) {
ctx.passes[] = {
type: 'imgui',
target: 'screen',
draw: layer.draw
}
}
function compile_plane(plane_config, ctx, group_effects) {
var plane_name = plane_config.plane || plane_config.name
var res = plane_config.resolution || ctx.screen_size
var camera = plane_config.camera
var layer_sort = plane_config.layer_sort || {} // layer -> 'y' or 'explicit'
// Build set of groups used as masks (these should not be drawn directly)
var mask_groups = {}
arrfor(array(group_effects), gname => {
var effects = group_effects[gname].effects || []
var e = 0
for (e = 0; e < length(effects); e++) {
if (effects[e].type == 'mask' && effects[e].mask_group)
mask_groups[effects[e].mask_group] = true
}
})
// Get all sprites in this plane
var all_sprites = film2d.query({plane: plane_name})
// Add manual drawables
var di = 0
if (plane_config.drawables) {
for (di = 0; di < length(plane_config.drawables); di++)
all_sprites[] = plane_config.drawables[di]
}
// Find which sprites belong to groups with effects
var effect_groups = {} // group_name -> {sprites: [], effects: []}
var base_sprites = []
var si = 0
var s = null
var sprite_groups = null
var assigned = false
var is_mask_only = false
var g = 0
var gname = null
for (si = 0; si < length(all_sprites); si++) {
s = all_sprites[si]
sprite_groups = s.groups || []
assigned = false
is_mask_only = length(sprite_groups) > 0
// First pass: check if sprite has any non-mask group
for (g = 0; g < length(sprite_groups); g++) {
gname = sprite_groups[g]
if (!mask_groups[gname]) {
is_mask_only = false
break
}
}
// Second pass: assign to effect groups
for (g = 0; g < length(sprite_groups); g++) {
gname = sprite_groups[g]
if (group_effects[gname]) {
if (!effect_groups[gname])
effect_groups[gname] = {sprites: [], effects: group_effects[gname].effects}
effect_groups[gname].sprites[] = s
assigned = true
break // Only assign to first matching effect group
}
}
// Add to base sprites if not assigned to effect group and not mask-only
if (!assigned && !is_mask_only) base_sprites[] = s
}
// Allocate plane target
var plane_target = ctx.alloc(res.width, res.height, plane_config.name)
// Always clear plane target to prevent stale data between frames
ctx.passes[] = {type: 'clear', target: plane_target, color: plane_config.clear || {r: 0, g: 0, b: 0, a: 0}}
// Render each effect group to temp target, apply effects, composite back
arrfor(array(effect_groups), gname => {
var eg = effect_groups[gname]
if (length(eg.sprites) == 0) return
var group_target = ctx.alloc(res.width, res.height, gname + '_content')
// Render group content
ctx.passes[] = {
type: 'render',
renderer: 'film2d',
drawables: eg.sprites,
camera: camera,
target: group_target,
target_size: res,
layer_sort: layer_sort,
clear: {r: 0, g: 0, b: 0, a: 0}
}
// Apply effects
var current = group_target
var e = 0
var effect = null
for (e = 0; e < length(eg.effects); e++) {
effect = eg.effects[e]
current = apply_effect(ctx, effect, current, {size: res, camera: camera, hint: gname, current_plane: plane_name, group_effects: group_effects})
}
// Composite result to plane
ctx.passes[] = {
type: 'composite',
source: current,
dest: plane_target,
source_size: res,
dest_size: res,
blend: 'over'
}
})
// Render base sprites (no effects)
if (length(base_sprites) > 0) {
ctx.passes[] = {
type: 'render',
renderer: 'film2d',
drawables: base_sprites,
camera: camera,
target: plane_target,
target_size: res,
layer_sort: layer_sort,
clear: null // Don't clear, blend on top
}
}
// Composite plane to screen
ctx.passes[] = {
type: 'blit_to_screen',
source: plane_target,
source_size: res,
dest_size: ctx.screen_size,
presentation: plane_config.presentation || 'stretch'
}
}
function apply_effect(ctx, effect, input, params) {
var size = params.size
var camera = params.camera
var hint = params.hint
var current_plane = params.current_plane
var group_effects = params.group_effects
var output = ctx.alloc(size.width, size.height, hint + '_' + effect.type)
var bright = null
var blur1 = null
var blur2 = null
var blur_passes = null
var blur_in = null
var p = 0
var mask_group = null
var mask_sprites = null
var mask_target = null
if (effect.type == 'bloom') {
bright = ctx.alloc(size.width, size.height, hint + '_bright')
blur1 = ctx.alloc(size.width, size.height, hint + '_blur1')
blur2 = ctx.alloc(size.width, size.height, hint + '_blur2')
// Threshold
ctx.passes[] = {
type: 'shader_pass',
shader: 'threshold',
input: input,
output: bright,
uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1}
}
// Blur passes
blur_passes = effect.blur_passes != null ? effect.blur_passes : 2
blur_in = bright
for (p = 0; p < blur_passes; p++) {
ctx.passes[] = {type: 'shader_pass', shader: 'blur', input: blur_in, output: blur1, uniforms: {direction: {x: 1, y: 0}, texel_size: {x: 1/size.width, y: 1/size.height}}}
ctx.passes[] = {type: 'shader_pass', shader: 'blur', input: blur1, output: blur2, uniforms: {direction: {x: 0, y: 1}, texel_size: {x: 1/size.width, y: 1/size.height}}}
blur_in = blur2
}
// Composite bloom
ctx.passes[] = {type: 'composite_textures', base: input, overlay: blur_in, output: output, mode: 'add'}
} else if (effect.type == 'mask') {
mask_group = effect.mask_group
// Query masks within the same plane to avoid cross-plane mask issues
mask_sprites = film2d.query({group: mask_group, plane: current_plane})
log.compositor("mask effect: group=" + mask_group + " plane=" + current_plane + " sprites=" + text(length(mask_sprites)))
if (length(mask_sprites) > 0) {
mask_target = ctx.alloc(size.width, size.height, hint + '_mask')
// Render mask
ctx.passes[] = {
type: 'render',
renderer: 'film2d',
drawables: mask_sprites,
camera: camera,
target: mask_target,
target_size: size,
clear: {r: 0, g: 0, b: 0, a: 0}
}
// Apply mask
ctx.passes[] = {
type: 'apply_mask',
content: input,
mask: mask_target,
output: output,
mode: effect.channel || 'alpha',
invert: effect.invert || false
}
} else {
// No mask sprites, pass through
ctx.passes[] = {type: 'blit', source: input, dest: output}
}
} else {
// Unknown effect, pass through
ctx.passes[] = {type: 'blit', source: input, dest: output}
}
return output
}
// Execute compiled plan
compositor.execute = function(plan) {
var cache = {}
arrfor(array(plan.targets), key => {
var spec = plan.targets[key]
cache[key] = backend.get_or_create_target(spec.width, spec.height, key)
})
function resolve(t) {
if (t == 'screen') return 'screen'
if (t && t.type == 'target') return cache[t.key]
return t
}
var commands = []
var i = 0
var pass = null
var target = null
var result = null
var c = 0
var rect = null
var src = null
var dst = null
for (i = 0; i < length(plan.passes); i++) {
pass = plan.passes[i]
if (pass.type == 'clear') {
target = resolve(pass.target)
commands[] = {cmd: 'begin_render', target: target, clear: pass.color}
commands[] = {cmd: 'end_render'}
} else if (pass.type == 'render') {
result = film2d.render({
drawables: pass.drawables,
camera: pass.camera,
target: resolve(pass.target),
target_size: pass.target_size,
layer_sort: pass.layer_sort || {},
clear: pass.clear
}, backend)
for (c = 0; c < length(result.commands); c++)
commands[] = result.commands[c]
} else if (pass.type == 'shader_pass') {
commands[] = {
cmd: 'shader_pass',
shader: pass.shader,
input: resolve(pass.input),
output: resolve(pass.output),
uniforms: pass.uniforms
}
} else if (pass.type == 'composite_textures') {
commands[] = {
cmd: 'composite_textures',
base: resolve(pass.base),
overlay: resolve(pass.overlay),
output: resolve(pass.output),
mode: pass.mode
}
} else if (pass.type == 'apply_mask') {
commands[] = {
cmd: 'apply_mask',
content_texture: resolve(pass.content),
mask_texture: resolve(pass.mask),
output: resolve(pass.output),
mode: pass.mode,
invert: pass.invert
}
} else if (pass.type == 'composite') {
commands[] = {
cmd: 'blit',
texture: resolve(pass.source),
target: resolve(pass.dest),
dst_rect: {x: 0, y: 0, width: pass.dest_size.width, height: pass.dest_size.height}
}
} else if (pass.type == 'blit_to_screen') {
rect = _calc_presentation(pass.source_size, pass.dest_size, pass.presentation)
commands[] = {
cmd: 'blit',
texture: resolve(pass.source),
target: 'screen',
dst_rect: rect,
filter: pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
}
} else if (pass.type == 'blit') {
src = resolve(pass.source)
dst = resolve(pass.dest)
commands[] = {
cmd: 'blit',
texture: src,
target: dst,
dst_rect: {x: 0, y: 0, width: dst.width, height: dst.height}
}
} else if (pass.type == 'imgui') {
commands[] = {
cmd: 'imgui',
target: resolve(pass.target),
draw: pass.draw
}
}
}
return {commands: commands, plan: plan}
}
function _calc_presentation(src, dst, mode) {
if (mode == 'stretch')
return {x: 0, y: 0, width: dst.width, height: dst.height}
var sx = 0
var sy = 0
var s = 0
var w = 0
var h = 0
var scale = 0
if (mode == 'integer_scale') {
sx = floor(dst.width / src.width)
sy = floor(dst.height / src.height)
s = max(1, min(sx, sy))
w = src.width * s
h = src.height * s
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
// letterbox
scale = min(dst.width / src.width, dst.height / src.height)
w = src.width * scale
h = src.height * scale
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
var _last_plan = null
var _orig_compile = compositor.compile
compositor.compile = function(config) {
_last_plan = _orig_compile(config)
return _last_plan
}
compositor.snapshot = function() {
if (!_last_plan) return null
var planes = []
var i = 0
var pass = null
for (i = 0; i < length(_last_plan.passes); i++) {
pass = _last_plan.passes[i]
if (pass.type == 'render') {
planes[] = {
drawable_count: length(pass.drawables),
camera: pass.camera ? {
pos: pass.camera.pos,
width: pass.camera.width,
height: pass.camera.height
} : null,
target_size: pass.target_size
}
}
}
return {
pass_count: length(_last_plan.passes),
target_count: length(array(_last_plan.targets)),
screen_size: _last_plan.screen_size,
planes: planes
}
}
return compositor

View File

@@ -1,345 +0,0 @@
var input = use('input')
return {}
var downkeys = {};
function keyname(key)
{
var str = input.keyname(key);
return str.toLowerCase();
}
function modstr(mod = input.keymod()) {
var s = "";
if (mod.ctrl) s += "C-";
if (mod.alt) s += "M-";
if (mod.super) s += "S-";
return s;
}
prosperon.on('key_down', function key_down(e) {
downkeys[e.key] = true;
var emacs = modstr(e.mod) + keyname(e.key);
if (e.repeat) player[0].raw_input(emacs, "rep");
else player[0].raw_input(emacs, "pressed");
})
prosperon.on('quit', function() {
os.exit(0);
})
prosperon.on('key_up', function key_up(e) {
delete downkeys[e.key];
var emacs = modstr(e.mod) + keyname(e.key);
player[0].raw_input(emacs, "released");
})
prosperon.on('drop_file', function (path) {
player[0].raw_input("drop", "pressed", path);
})
var mousepos = [0, 0];
prosperon.on('text_input', function (e) {
player[0].raw_input("char", "pressed", e.text);
})
prosperon.on('mouse_motion', function (e)
{
mousepos = e.pos;
player[0].mouse_input("move", e.pos, e.d_pos);
})
prosperon.on('mouse_wheel', function mousescroll(e) {
player[0].mouse_input(modstr() + "scroll", e.scroll);
})
prosperon.on('mouse_button_down', function(e) {
player[0].mouse_input(modstr() + e.button, "pressed");
input.mouse.buttons[e.button] = true
})
prosperon.on('mouse_button_up', function(e) {
player[0].mouse_input(modstr() + e.button, "released");
input.mouse.buttons[e.button] = false
})
input.mouse = {};
input.mouse.screenpos = function mouse_screenpos() {
return mousepos.slice();
};
input.mouse.worldpos = function mouse_worldpos() {
return prosperon.camera.screen2world(mousepos);
};
input.mouse.viewpos = function mouse_viewpos()
{
var world = input.mouse.worldpos();
return mousepos.slice();
}
input.mouse.disabled = function mouse_disabled() {
input.mouse_mode(1);
};
input.mouse.normal = function mouse_normal() {
input.mouse_mode(0);
};
input.mouse.mode = function mouse_mode(m) {
if (input.mouse.custom[m]) input.cursor_img(input.mouse.custom[m]);
else input.mouse_cursor(m);
};
input.mouse.buttons = {
0:false,
1:false,
2:false
}
input.mouse.set_custom_cursor = function mouse_cursor(img, mode = input.mouse.cursor.default) {
if (!img) delete input.mouse.custom[mode];
else {
input.cursor_img(img);
input.mouse.custom[mode] = img;
}
};
input.mouse.doc = {};
input.mouse.doc.pos = "The screen position of the mouse.";
input.mouse.doc.worldpos = "The position in the game world of the mouse.";
input.mouse.disabled.doc = "Set the mouse to hidden. This locks it to the game and hides it, but still provides movement and click events.";
input.mouse.normal.doc = "Set the mouse to show again after hiding.";
input.keyboard = {};
input.keyboard.down = function (code) {
if (typeof code == "number") return downkeys[code];
if (typeof code == "string") return downkeys[code.toUpperCase().charCodeAt()] || downkeys[code.toLowerCase().charCodeAt()];
return null;
};
input.print_pawn_kbm = function (pawn) {
if (!("inputs" in pawn)) return;
var str = "";
for (var key in pawn.inputs) {
if (!pawn.inputs[key].doc) continue;
str += `${key} | ${pawn.inputs[key].doc}\n`;
}
return str;
};
var joysticks = {};
joysticks["wasd"] = {
uy: "w",
dy: "s",
ux: "d",
dx: "a",
};
input.procdown = function procdown() {
for (var k in downkeys) player[0].raw_input(keyname(k), "down");
for (var i in joysticks) {
var joy = joysticks[i];
var x = joy.ux - joy.dx;
var y = joy.uy - joy.dy;
player[0].joy_input(i, joysticks[i]);
}
};
input.print_md_kbm = function print_md_kbm(pawn) {
if (!("inputs" in pawn)) return;
var str = "";
str += "|control|description|\n|---|---|\n";
for (var key in pawn.inputs) {
str += `|${key}|${pawn.inputs[key].doc}|`;
str += "\n";
}
return str;
};
input.has_bind = function (pawn, bind) {
return typeof pawn.inputs?.[bind] == "function";
};
input.action = {
add_new(name) {
var action = Object.create(input.action);
action.name = name;
action.inputs = [];
this.actions.push(action);
return action;
},
actions: [],
};
input.tabcomplete = function tabcomplete(val, list) {
if (!val) return val;
list = filter(x => x.startsWith(val))
if (list.length == 1) {
return list[0];
}
var ret = null;
var i = val.length;
while (!ret && list.length != 0) {
var char = list[0][i];
if (
!list.every(function (x) {
return x[i] == char;
})
)
ret = list[0].slice(0, i);
else {
i++;
list = list.filter(x => x.length-1 > i)
}
}
return ret ? ret : val;
};
/* May be a human player; may be an AI player */
/*
'block' on a pawn's input blocks any input from reaching below for the
*/
var Player = {
players: [],
input(fn, ...args) {
this.pawns.forEach(x => x[fn]?.(...args));
},
mouse_input(type, ...args) {
for (var pawn of [...this.pawns].reverse()) {
if (typeof pawn.inputs?.mouse?.[type] == "function") {
pawn.inputs.mouse[type].call(pawn, ...args);
pawn.inputs.post?.call(pawn);
if (!pawn.inputs.fallthru) return;
}
}
},
char_input(c) {
for (var pawn of [...this.pawns].reverse()) {
if (typeof pawn.inputs?.char == "function") {
pawn.inputs.char.call(pawn, c);
pawn.inputs.post?.call(pawn);
if (!pawn.inputs.fallthru) return;
}
}
},
joy_input(name, joystick) {
for (var pawn of [...this.pawns].reverse()) {
if (!pawn.inputs) return;
if (!pawn.inputs.joystick) return;
if (!pawn.inputs.joystick[name]) return;
var x = 0;
if (input.keyboard.down(joystick.ux)) x++;
if (input.keyboard.down(joystick.dx)) x--;
var y = 0;
if (input.keyboard.down(joystick.uy)) y++;
if (input.keyboard.down(joystick.dy)) y--;
pawn.inputs.joystick[name](x, y);
}
},
raw_input(cmd, state, ...args) {
for (var pawn of [...this.pawns].reverse()) {
var inputs = pawn.inputs;
if (!inputs[cmd]) {
if (inputs.block) return;
continue;
}
var fn = null;
switch (state) {
case "pressed":
fn = inputs[cmd];
break;
case "rep":
fn = inputs[cmd].rep ? inputs[cmd] : null;
break;
case "released":
fn = inputs[cmd].released;
break;
case "down":
if (typeof inputs[cmd].down == "function") fn = inputs[cmd].down;
else if (inputs[cmd].down) fn = inputs[cmd];
}
var consumed = false;
if (typeof fn == "function") {
fn.call(pawn, ...args);
consumed = true;
}
if (state == "released") inputs.release_post?.call(pawn);
if (inputs.block) return;
if (consumed) return;
}
},
obj_controlled(obj) {
for (var p in Player.players) {
if (p.pawns.has(obj)) return true;
}
return false;
},
print_pawns() {
[...this.pawns].reverse().forEach(x => log.console(x))
},
create() {
var n = Object.create(this);
n.pawns = new Set()
n.gamepads = [];
this.players.push(n);
this[this.players.length - 1] = n;
return n;
},
control(pawn) {
if (!pawn)
return
if (!pawn.inputs)
throw new Error("attempted to control a pawn without any input object.");
this.pawns.add(pawn);
},
uncontrol(pawn) {
this.pawns.delete(pawn)
},
};
input.do_uncontrol = function input_do_uncontrol(pawn) {
if (!pawn.inputs) return;
Player.players.forEach(function (p) {
p.pawns.delete(pawn)
});
};
//for (var i = 0; i < 4; i++)
Player.create();
Player.control.doc = "Control a provided object, if the object has an 'inputs' object.";
Player.uncontrol.doc = "Uncontrol a previously controlled object.";
Player.print_pawns.doc = "Print out a list of the current pawn control stack.";
Player.doc = {};
Player.doc.players = "A list of current players.";
var player = Player;
input.player = Player
return input

316
core.cm Normal file
View File

@@ -0,0 +1,316 @@
// core.cm - Minimal entry point for prosperon rendering
//
// Usage:
// var core = use('prosperon/core')
// core.start({
// width: 1280,
// height: 720,
// title: "My Game",
// update: function(dt) { ... },
// render: function() { return graph }
// })
//
// Headless mode (no window, no input, no render, no audio):
// core.start({ headless: true, update: function(dt) { ... } })
var io = use('cellfs')
io.mount('/Users/john/work/prosperon')
var time_mod = use('time')
var core = {}
// Private state
var _running = false
var _config = null
var _backend = null
var _window = null
var _last_time = 0
var _framerate = 60
// Lazy-loaded modules (only in non-headless mode)
var video = null
var events = null
var imgui = null
var debug_imgui = null
// Start the application
core.start = function(config) {
_config = config
_framerate = config.framerate || 60
_running = true
_last_time = time_mod.number()
if (config.probe) _register_probes()
if (config.headless) {
_headless_loop()
return true
}
// Load SDL modules
video = use('sdl3/video')
events = use('sdl3/input')
imgui = use('imgui')
debug_imgui = use('debug_imgui')
// Initialize SDL GPU backend
var sdl_gpu = use('sdl_gpu')
_backend = sdl_gpu
var init_result = _backend.init({
width: config.width || 1280,
height: config.height || 720,
title: config.title || "Prosperon"
})
if (!init_result) {
log.console("core: Failed to initialize backend")
return false
}
_window = _backend.get_window()
if ((config.imgui || config.editor) && imgui.init) {
imgui.init(_window, _backend.get_device())
}
// Start main loop
_main_loop()
// Call start callback after the first frame and main loop are established
if (config.start) config.start()
return true
}
// Stop the application
core.stop = function() {
_running = false
}
// Get window size
core.window_size = function() {
return _backend.get_window_size()
}
// Get backend for direct access
core.backend = function() {
return _backend
}
// FPS tracking
var _fps_samples = []
var _fps_sample_count = 120
var _fps_sample_sum = 0
var _fps_sample_pos = 0
function fps_add_sample(sample) {
var n = length(_fps_samples)
var old = null
if (n < _fps_sample_count) {
_fps_samples[] = sample
_fps_sample_sum += sample
} else {
old = _fps_samples[_fps_sample_pos]
_fps_samples[_fps_sample_pos] = sample
_fps_sample_sum += sample - old
_fps_sample_pos++
if (_fps_sample_pos >= _fps_sample_count) _fps_sample_pos = 0
}
}
function fps_get_avg() {
var n = length(_fps_samples)
return n ? _fps_sample_sum / n : 0
}
var _current_fps = 0
var _frame_time_ms = 0
// Headless loop — update only, no window/input/render/audio
function _headless_loop() {
if (!_running) return
var now = time_mod.number()
var dt = now - _last_time
_last_time = now
if (_config.update) _config.update(dt)
var frame_time = 1 / _framerate
var elapsed = time_mod.number() - now
var delay = frame_time - elapsed
if (delay < 0) delay = 0
$delay(_headless_loop, delay)
}
// Main loop
function _main_loop() {
var frame_start = time_mod.number()
if (!_running) return
var now = frame_start
var dt = now - _last_time
_last_time = now
// Process events
var evts = events.get_events()
var win_size = _backend.get_window_size()
// Get input module for auto-ingestion
var input_mod = use('input')
arrfor(evts, function(ev) {
if (_config.imgui || _config.editor) {
imgui.process_event(ev)
}
var ev_obj = events.objectify(ev)
if (ev_obj.type == 'quit') {
_running = false
$stop()
return
}
if (ev_obj.type == 'window_pixel_size_changed') {
win_size.width = ev_obj.width
win_size.height = ev_obj.height
if (_backend.set_window_size) {
_backend.set_window_size(ev_obj.width, ev_obj.height)
}
}
// Auto-ingest through input system
input_mod.ingest(ev_obj)
// Optional raw hook for debugging
if (_config.input) {
_config.input(ev_obj)
}
})
// Update
if (_config.update) {
_config.update(dt)
}
var imgui_mod = use('imgui')
var debug_imgui = use('debug_imgui')
// ImGui Frame
if (_config.imgui || _config.editor) {
imgui_mod.newframe()
}
// Render
var render_result = null
var dbg = false
var stats = null
if (_config.render) {
render_result = _config.render()
if (render_result) {
if (_config.debug == 'graph') {
log.console(render_result)
$stop()
return
}
dbg = _config.debug == 'cmd'
// Build stats for debug_imgui
stats = {
fps: _current_fps,
frame_time_ms: _frame_time_ms
}
// Handle both compositor result ({commands: [...]}) and fx_graph (graph object)
if (render_result.commands) {
if (_config.imgui || _config.editor) {
render_result.commands[] = {
cmd: 'imgui',
draw: function(ui) {
if (_config.imgui) _config.imgui(ui)
if (is_function(_config.editor)) {
debug_imgui.render(ui, null, render_result.plan, stats)
_config.editor(ui)
}
},
target: 'screen'
}
}
// Compositor result - execute commands directly
_backend.execute_commands(render_result.commands, win_size, dbg)
} else {
// fx_graph result - execute graph
_backend.execute_graph(render_result, win_size, dbg)
}
if (dbg) {
$stop()
return
}
}
}
var frame_end = time_mod.number()
var actual_frame_time = frame_end - frame_start
_frame_time_ms = actual_frame_time * 1000
fps_add_sample(actual_frame_time)
var avg_frame_time = fps_get_avg()
_current_fps = avg_frame_time > 0 ? 1 / avg_frame_time : 0
// Schedule next frame
var frame_time = 1 / _framerate
var elapsed = frame_end - frame_start
var delay = frame_time - elapsed
if (delay < 0) delay = 0
$delay(_main_loop, delay)
}
function _register_probes() {
var probe = use('probe')
var film2d = use('film2d')
var world = use('world')
var comp = use('compositor')
var input_mod = use('input')
var tween_mod = use('tween')
var graphics_mod = use('graphics')
probe.register("drawables", {
all: function(args) { return film2d.snapshot() }
})
probe.register("world", {
all: function(args) { return world.snapshot() },
count: function(args) { return world.count() }
})
probe.register("compositor", {
all: function(args) { return comp.snapshot() }
})
probe.register("input", {
all: function(args) { return input_mod.snapshot() }
})
probe.register("tweens", {
all: function(args) { return tween_mod.snapshot() }
})
probe.register("assets", {
all: function(args) { return graphics_mod.snapshot() }
})
probe.register("core", {
fps: function(args) {
return {fps: _current_fps, frame_time_ms: _frame_time_ms}
}
})
}
return core

118
datastream.c Normal file
View File

@@ -0,0 +1,118 @@
#include "cell.h"
#define PL_MPEG_IMPLEMENTATION
#include "pl_mpeg.h"
struct datastream {
plm_t *plm;
JSValue callback;
JSContext *js;
int width;
int height;
};
typedef struct datastream datastream;
#include "cell.h"
#include "limits.h"
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include "cbuf.h"
void datastream_free(JSRuntime *rt,datastream *ds)
{
plm_destroy(ds->plm);
free(ds);
}
QJSCLASS(datastream,)
struct datastream *ds_openvideo(void *raw, size_t rawlen)
{
struct datastream *ds = malloc(sizeof(*ds));
void *newraw = malloc(rawlen);
memcpy(newraw,raw,rawlen);
ds->plm = plm_create_with_memory(newraw, rawlen, 1);
if (!ds->plm) {
free(ds);
free(newraw);
return NULL;
}
return ds;
}
void ds_advance(struct datastream *ds, double s) {
plm_decode(ds->plm, s);
}
void ds_seek(struct datastream *ds, double time) {
plm_seek(ds->plm, time, false);
}
double ds_length(struct datastream *ds) {
return plm_get_duration(ds->plm);
}
JSC_CCALL(datastream_time, return number2js(js,plm_get_time(js2datastream(js,self)->plm)); )
JSC_CCALL(datastream_seek, ds_seek(js2datastream(js,self), js2number(js,argv[0])))
JSC_CCALL(datastream_advance, ds_advance(js2datastream(js,self), js2number(js,argv[0])))
JSC_CCALL(datastream_duration, return number2js(js,ds_length(js2datastream(js,self))))
JSC_CCALL(datastream_framerate, return number2js(js,plm_get_framerate(js2datastream(js,self)->plm)))
static const JSCFunctionListEntry js_datastream_funcs[] = {
MIST_FUNC_DEF(datastream, time, 0),
MIST_FUNC_DEF(datastream, seek, 1),
MIST_FUNC_DEF(datastream, advance, 1),
MIST_FUNC_DEF(datastream, duration, 0),
MIST_FUNC_DEF(datastream, framerate, 0),
};
static void render_frame(plm_t *mpeg, plm_frame_t *frame, void *d) {
datastream *ds = d;
if (JS_IsNull(ds->callback)) return;
uint8_t *rgb = malloc(frame->height*frame->width*4);
memset(rgb,255,frame->height*frame->width*4);
plm_frame_to_rgba(frame, rgb, frame->width*4);
// Create surface data object instead of SDL_Surface
JS_FRAME(ds->js);
JS_ROOT(surfData, JS_NewObject(ds->js));
JS_SetPropertyStr(ds->js, surfData.val, "width", JS_NewInt32(ds->js, frame->width));
JS_SetPropertyStr(ds->js, surfData.val, "height", JS_NewInt32(ds->js, frame->height));
JSValue tmp = JS_NewString(ds->js, "rgba32");
JS_SetPropertyStr(ds->js, surfData.val, "format", tmp);
JS_SetPropertyStr(ds->js, surfData.val, "pitch", JS_NewInt32(ds->js, frame->width*4));
tmp = js_new_blob_stoned_copy(ds->js, rgb, frame->height*frame->width*4);
JS_SetPropertyStr(ds->js, surfData.val, "pixels", tmp);
JSValue s[1];
s[0] = surfData.val;
JSValue cb = ds->callback;
JSValue cret = JS_Call(ds->js, cb, JS_NULL, 1, s);
JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
free(rgb);
// TODO: raise exception
}
JSC_CCALL(os_make_video,
size_t len;
void *data = js_get_blob_data(js,&len,argv[0]);
if (data == -1 || !data) return JS_EXCEPTION;
datastream *ds = ds_openvideo(data, len);
if (!ds) return JS_ThrowReferenceError(js, "Video file was not valid.");
ds->js = js;
ds->callback = JS_NULL;
plm_set_video_decode_callback(ds->plm, render_frame, ds);
return datastream2js(js,ds);
)
static const JSCFunctionListEntry js_video_funcs[] = {
MIST_FUNC_DEF(os, make_video, 1),
};
CELL_USE_FUNCS(js_video_funcs)

498
debug_imgui.cm Normal file
View File

@@ -0,0 +1,498 @@
// debug_imgui.cm - ImGui Debug Windows for Render Architecture
//
// Provides debug windows for inspecting:
// - Scene tree and node properties
// - Render graph and pass connections
// - Effect parameters
// - Performance statistics
var debug_imgui = {}
var json = use('json')
// State
var _show_scene_tree = false
var _show_render_graph = false
var _show_effects = false
var _show_stats = true
var _show_targets = false
var _selected_node = null
var _selected_pass = null
var _expanded_nodes = {}
// Toggle windows
debug_imgui.toggle_scene_tree = function() { _show_scene_tree = !_show_scene_tree }
debug_imgui.toggle_render_graph = function() { _show_render_graph = !_show_render_graph }
debug_imgui.toggle_effects = function() { _show_effects = !_show_effects }
debug_imgui.toggle_stats = function() { _show_stats = !_show_stats }
debug_imgui.toggle_targets = function() { _show_targets = !_show_targets }
// Main render function - call from imgui callback
debug_imgui.render = function(imgui, scene_graph, render_plan, stats) {
// Menu bar for toggling windows
_render_menu(imgui)
if (_show_scene_tree && scene_graph) {
_render_scene_tree(imgui, scene_graph)
}
if (_show_render_graph && render_plan) {
_render_graph_view(imgui, render_plan)
}
if (_show_effects) {
_render_effects_panel(imgui)
}
if (_show_stats && stats) {
_render_stats(imgui, stats)
}
if (_show_targets && render_plan) {
_render_targets(imgui, render_plan)
}
if (_selected_node) {
_render_node_inspector(imgui, _selected_node)
}
if (_selected_pass) {
_render_pass_inspector(imgui, _selected_pass)
}
}
// Render debug menu
function _render_menu(imgui) {
imgui.mainmenubar(function() {
imgui.menu("Debug", function() {
if (imgui.menuitem("Scene Tree", null, function(){}, _show_scene_tree)) {
_show_scene_tree = !_show_scene_tree
}
if (imgui.menuitem("Render Graph", null, function(){}, _show_render_graph)) {
_show_render_graph = !_show_render_graph
}
if (imgui.menuitem("Effects", null, function(){}, _show_effects)) {
_show_effects = !_show_effects
}
if (imgui.menuitem("Statistics", null, function(){}, _show_stats)) {
_show_stats = !_show_stats
}
if (imgui.menuitem("Render Targets", null, function(){}, _show_targets)) {
_show_targets = !_show_targets
}
})
})
}
// Render scene tree window
function _render_scene_tree(imgui, scene_graph) {
imgui.window("Scene Tree", function() {
if (!scene_graph.root) {
imgui.text("No scene root")
return
}
imgui.text("Total nodes: " + text(scene_graph.stats.total_nodes))
imgui.text("Dirty this frame: " + text(scene_graph.stats.dirty_this_frame))
imgui.text("Geometry rebuilds: " + text(scene_graph.stats.geometry_rebuilds))
imgui.text("---")
_render_node_tree(imgui, scene_graph.root, 0)
})
}
// Render a node and its children recursively
function _render_node_tree(imgui, node, depth) {
if (!node) return
var id = node.id || 'unknown'
var type = node.type || 'node'
var label = type + " [" + id + "]"
// Add dirty indicator
if (is_number(node.dirty) && node.dirty > 0) {
label += " *"
}
var has_children = node.children && length(node.children) > 0
if (has_children) {
imgui.tree(label, function() {
// Show node summary
_render_node_summary(imgui, node)
// Recurse children
var i = 0
for (i = 0; i < length(node.children); i++) {
_render_node_tree(imgui, node.children[i], depth + 1)
}
})
} else {
// Leaf node - show as selectable
if (imgui.button(label)) {
_selected_node = node
}
imgui.sameline(0)
_render_node_summary(imgui, node)
}
}
// Render node summary inline
function _render_node_summary(imgui, node) {
var info = []
if (node.pos) {
info[] = "pos:(" + text(round(node.pos.x)) + "," + text(round(node.pos.y)) + ")"
}
if (node.width && node.height) {
info[] = "size:" + text(node.width) + "x" + text(node.height)
}
if (node.image) {
info[] = "img:" + node.image
}
var t = null
if (node.text) {
t = node.text
if (length(t) > 20) t = text(t, 0, 17) + "..."
info[] = "\"" + t + "\""
}
var fx = []
var j = 0
if (node.effects && length(node.effects) > 0) {
for (j = 0; j < length(node.effects); j++) {
fx[] = node.effects[j].type
}
info[] = "fx:[" + text(fx, ",") + "]"
}
if (length(info) > 0) {
imgui.text(" " + text(info, " "))
}
}
// Render node inspector window
function _render_node_inspector(imgui, node) {
imgui.window("Node Inspector", function() {
if (imgui.button("Close")) {
_selected_node = null
return
}
imgui.text("ID: " + (node.id || 'none'))
imgui.text("Type: " + (node.type || 'unknown'))
imgui.text("Layer: " + text(node.layer || 0))
imgui.text("Dirty: " + text(node.dirty || 0))
imgui.text("---")
// Position
var pos = null
if (node.pos) {
imgui.text("Position")
pos = imgui.slider("X", node.pos.x, -1000, 1000)
if (pos != node.pos.x) node.pos.x = pos
pos = imgui.slider("Y", node.pos.y, -1000, 1000)
if (pos != node.pos.y) node.pos.y = pos
}
// Size
if (node.width != null) {
imgui.text("Size")
node.width = imgui.slider("Width", node.width, 0, 1000)
node.height = imgui.slider("Height", node.height, 0, 1000)
}
// Opacity
if (node.opacity != null) {
node.opacity = imgui.slider("Opacity", node.opacity, 0, 1)
}
// Color
if (node.color) {
imgui.text("Color")
node.color.r = imgui.slider("R", node.color.r, 0, 1)
node.color.g = imgui.slider("G", node.color.g, 0, 1)
node.color.b = imgui.slider("B", node.color.b, 0, 1)
node.color.a = imgui.slider("A", node.color.a, 0, 1)
}
// World transform (read-only)
if (node.world_pos) {
imgui.text("---")
imgui.text("World Position: (" + text(round(node.world_pos.x * 100) / 100) + ", " + text(round(node.world_pos.y * 100) / 100) + ")")
}
if (node.world_opacity != null) {
imgui.text("World Opacity: " + text(round(node.world_opacity * 100) / 100))
}
// Image
if (node.image) {
imgui.text("---")
imgui.text("Image: " + node.image)
}
// Text
if (node.text != null) {
imgui.text("---")
imgui.text("Text: " + node.text)
if (node.font) imgui.text("Font: " + node.font)
if (node.size) imgui.text("Size: " + text(node.size))
}
// Effects
var ei = 0
var fx = null
if (node.effects && length(node.effects) > 0) {
imgui.text("---")
imgui.text("Effects:")
for (ei = 0; ei < length(node.effects); ei++) {
fx = node.effects[ei]
imgui.tree(fx.type, function() {
arrfor(array(fx), k => {
var v = fx[k]
if (k != 'type' && k != 'source') {
if (is_number(v)) {
fx[k] = imgui.slider(k, v, 0, 10)
} else {
imgui.text(k + ": " + text(v))
}
}
})
})
}
}
// Geometry cache info
if (node.geom_cache) {
imgui.text("---")
imgui.text("Geometry Cache:")
imgui.text(" Vertices: " + text(node.geom_cache.vert_count || 0))
imgui.text(" Indices: " + text(node.geom_cache.index_count || 0))
if (node.geom_cache.texture_key) {
imgui.text(" Texture: " + node.geom_cache.texture_key)
}
}
})
}
// Render graph view window
function _render_graph_view(imgui, plan) {
imgui.window("Render Graph", function() {
if (!plan || !plan.passes) {
imgui.text("No render plan")
return
}
imgui.text("Passes: " + text(length(plan.passes)))
imgui.text("Targets: " + text(length(array(plan.targets || {}))))
imgui.text("Persistent: " + text(length(array(plan.persistent_targets || {}))))
imgui.text("---")
var i = 0
var pass = null
var label = null
var target_info = null
for (i = 0; i < length(plan.passes); i++) {
pass = plan.passes[i]
label = text(i) + ": " + pass.type
if (pass.shader) label += " [" + pass.shader + "]"
if (pass.renderer) label += " [" + pass.renderer + "]"
if (imgui.button(label)) {
_selected_pass = pass
}
// Show target info
imgui.sameline(0)
target_info = ""
if (pass.target) {
if (pass.target == 'screen') {
target_info = "-> screen"
} else if (pass.target.key) {
target_info = "-> " + pass.target.key
}
}
if (pass.output) {
if (pass.output.key) {
target_info = "-> " + pass.output.key
}
}
if (target_info) imgui.text(target_info)
}
})
}
// Render pass inspector
function _render_pass_inspector(imgui, pass) {
imgui.window("Pass Inspector", function() {
if (imgui.button("Close")) {
_selected_pass = null
return
}
imgui.text("Type: " + pass.type)
if (pass.shader) imgui.text("Shader: " + pass.shader)
if (pass.renderer) imgui.text("Renderer: " + pass.renderer)
if (pass.blend) imgui.text("Blend: " + pass.blend)
if (pass.presentation) imgui.text("Presentation: " + pass.presentation)
// Target info
imgui.text("---")
if (pass.target) {
if (pass.target == 'screen') {
imgui.text("Target: screen")
} else if (pass.target.key) {
imgui.text("Target: " + pass.target.key)
if (pass.target.w) imgui.text(" Size: " + text(pass.target.w) + "x" + text(pass.target.h))
}
}
if (pass.input) {
imgui.text("Input: " + (pass.input.key || 'unknown'))
}
if (pass.output) {
imgui.text("Output: " + (pass.output.key || 'unknown'))
}
// Uniforms
if (pass.uniforms) {
imgui.text("---")
imgui.text("Uniforms:")
arrfor(array(pass.uniforms), k => {
var v = pass.uniforms[k]
if (is_array(v)) {
imgui.text(" " + k + ": [" + text(v, ", ") + "]")
} else {
imgui.text(" " + k + ": " + text(v))
}
})
}
// Clear color
if (pass.color) {
imgui.text("---")
imgui.text("Clear: rgba(" +
text(round(pass.color.r * 255)) + "," +
text(round(pass.color.g * 255)) + "," +
text(round(pass.color.b * 255)) + "," +
text(round(pass.color.a * 100) / 100) + ")")
}
// Source size
if (pass.source_size) {
imgui.text("Source size: " + text(pass.source_size.w || pass.source_size.width) + "x" + text(pass.source_size.h || pass.source_size.height))
}
if (pass.dest_size) {
imgui.text("Dest size: " + text(pass.dest_size.w || pass.dest_size.width) + "x" + text(pass.dest_size.h || pass.dest_size.height))
}
})
}
// Render effects panel
function _render_effects_panel(imgui) {
var effects_mod = use('effects')
imgui.window("Effects Registry", function() {
var effect_list = effects_mod.list()
imgui.text("Registered effects: " + text(length(effect_list)))
imgui.text("---")
var i = 0
var name = null
var deff = null
for (i = 0; i < length(effect_list); i++) {
name = effect_list[i]
deff = effects_mod.get(name)
imgui.tree(name, function() {
imgui.text("Type: " + (deff.type || 'unknown'))
imgui.text("Requires target: " + (deff.requires_target ? "yes" : "no"))
if (deff.params) {
imgui.text("Parameters:")
arrfor(array(deff.params), k => {
var p = deff.params[k]
var info = k
if (p.default != null) info += " = " + text(p.default)
if (p.type) info += " (" + p.type + ")"
if (p.required) info += " [required]"
imgui.text(" " + info)
})
}
})
}
})
}
// Render statistics
function _render_stats(imgui, stats) {
imgui.window("Render Statistics", function() {
if (stats.scene) {
imgui.text("Scene:")
imgui.text(" Total nodes: " + text(stats.scene.total_nodes || 0))
imgui.text(" Dirty nodes: " + text(stats.scene.dirty_this_frame || 0))
imgui.text(" Geometry rebuilds: " + text(stats.scene.geometry_rebuilds || 0))
}
if (stats.render) {
imgui.text("---")
imgui.text("Render:")
imgui.text(" Draw calls: " + text(stats.render.draw_calls || 0))
imgui.text(" Triangles: " + text(stats.render.triangles || 0))
imgui.text(" Batches: " + text(stats.render.batches || 0))
}
if (stats.targets) {
imgui.text("---")
imgui.text("Targets:")
imgui.text(" Active: " + text(stats.targets.active || 0))
imgui.text(" Pooled: " + text(stats.targets.pooled || 0))
imgui.text(" Memory: " + text(stats.targets.memory_mb || 0) + " MB")
}
if (stats.fps) {
imgui.text("---")
imgui.text("FPS: " + text(round(stats.fps)))
imgui.text("Frame time: " + text(round(stats.frame_time_ms * 100) / 100) + " ms")
}
})
}
// Render targets view
function _render_targets(imgui, plan) {
imgui.window("Render Targets", function() {
if (!plan) {
imgui.text("No render plan")
return
}
imgui.text("Temporary Targets:")
if (plan.targets) {
arrfor(array(plan.targets), key => {
var t = plan.targets[key]
imgui.text(" " + key + ": " + text(t.width) + "x" + text(t.height))
})
}
imgui.text("---")
imgui.text("Persistent Targets:")
if (plan.persistent_targets) {
arrfor(array(plan.persistent_targets), key => {
var t = plan.persistent_targets[key]
imgui.text(" " + key + ": " + text(t.width) + "x" + text(t.height ))
})
}
})
}
return debug_imgui

221
docs/3pfollow.md Normal file
View File

@@ -0,0 +1,221 @@
---
draft: true
---
This was a good camera system for third person games. Recreate it eventually in cell.
// void ThirdPersonFollow::_ready() {
// Target = dynamic_cast<Spatial*>(get_node(TargetPath));
// ExternalFrame = dynamic_cast<Spatial*>(get_node(ExternalFramePath));
// }
// void ThirdPersonFollow::_process(float delta_time) {
// if (Target != nullptr) {
// // Get the frame of reference
// if (has_node(ExternalFramePath) && get_node(ExternalFramePath) != this)
// TFOR = dynamic_cast<Spatial*>(get_node(ExternalFramePath))->get_global_transform();
// else
// TFOR = Transform(Basis(glm::vec3::RIGHT(), glm::vec3::UP(), glm::vec3::FORWARD()), glm::vec3::ZERO());
// CalculateTargetOffset();
// TargetPosition = CalculatePosition();
// translate(to_local(FrameBasedVectorLerp(get_global_translation(), TargetPosition, PositionSpeeds, delta_time)));
// set_rotation_degrees(get_rotation_degrees().linear_interpolate(RotationOffset, RotationSpeed * delta_time));
// }
// // For rotation
// // TODO: Add rotation offset in properly
// //this->SetActorRotation(FMath::Lerp(this->GetActorRotation(), TargetRotation, RotationSpeed * DeltaTime));
// //this->set_rotation(glm::quat(this->get_rotation().slerp(RotationOffset, RotationSpeed * DeltaTime)).get_euler());
// //set_global_rotation_degrees(get_global_rotation_degrees().linear_interpolate(RotationOffset, RotationSpeed * delta_time));
// // TODO: Figure out how to translate rotation to a global rotation
// }
/*
glm::vec3 ThirdPersonFollow::CalculatePosition()
{
glm::vec3 p1 = CalculateCenter();
glm::vec3 p2 =
XDirPosts ? GetPostsOffset(TFOR.Right(),
FloatWidths.
x) : GetExtentsOffset(TFOR.Right(),
FloatWidths.x,
TargetOffset.x,
AnchorWidths.x);
glm::vec3 p3 =
YDirPosts ? GetPostsOffset(TFOR.Up(),
FloatWidths.
y) : GetExtentsOffset(TFOR.Up(),
FloatWidths.y,
TargetOffset.y,
AnchorWidths.y);
glm::vec3 p4 =
ZDirPosts ? GetPostsOffset(TFOR.Back(),
FloatWidths.
z) : GetExtentsOffset(TFOR.Back(),
FloatWidths.z,
TargetOffset.z,
AnchorWidths.z);
return p1 + p2 + p3 + p4;
}
glm::vec3 ThirdPersonFollow::CalculateCenter()
{
return Target->get_global_translation() +
TFOR.TransformDirection(Offset) + (mytransform->Back() * Distance);
}
glm::vec3 ThirdPersonFollow::
GetPostsOffset(const glm::vec3 & DirectionVector, float AnchorWidth)
{
float dot = glm::dot(Target->Forward(), DirectionVector);
return DirectionVector * (dot >= 0 ? AnchorWidth : AnchorWidth * -1);
}
glm::vec3 ThirdPersonFollow::
GetExtentsOffset(const glm::vec3 & DirectionVector, float AnchorWidth,
float TOffset, float Width)
{
float negated_offset_sign = ((0 <= TOffset) - (TOffset < 0)) * -1.f;
float TotalWidth = AnchorWidth + Width;
if (glm::abs(TOffset) > TotalWidth
&& !glm::epsilonEqual(glm::abs(TOffset), TotalWidth, 0.5f))
return DirectionVector * TotalWidth * negated_offset_sign;
else {
if (glm::abs(TOffset) >= AnchorWidth)
return DirectionVector * AnchorWidth * negated_offset_sign;
else
return DirectionVector * TOffset * -1.f;
}
return glm::vec3(0.f);
}
glm::vec3 ThirdPersonFollow::FrameBasedVectorLerp(const glm::vec3 & From,
const glm::vec3 & To,
const glm::vec3 & Speeds,
float Tick)
{
// Previously "FORTransform.TransformVector(Speeds)
glm::vec3 TSpeed = glm::abs(TFOR.TransformDirection(Speeds));
glm::vec3 TOffset = glm::abs(TFOR.TransformDirection(TargetOffset));
glm::vec3 TAnchorWidths =
glm::abs(TFOR.TransformDirection(AnchorWidths));
glm::vec3 TFloatWidths =
glm::abs(TFOR.TransformDirection(FloatWidths));
// If these are true, that means to use the anchor speed instead of the normal speed.
// True if the offset is beyond the anchor plus the width
bool bUseX = GetLerpParam(TOffset.x, TAnchorWidths.x, TFloatWidths.x);
bool bUseY = GetLerpParam(TOffset.y, TAnchorWidths.y, TFloatWidths.y);
bool bUseZ = GetLerpParam(TOffset.z, TAnchorWidths.z, TFloatWidths.z);
float xAlpha =
glm::clamp((bUseX ? AnchorSpeed : TSpeed.x) * Tick, 0.f, 1.f);
float yAlpha =
glm::clamp((bUseY ? AnchorSpeed : TSpeed.y) * Tick, 0.f, 1.f);
float zAlpha =
glm::clamp((bUseZ ? AnchorSpeed : TSpeed.z) * Tick, 0.f, 1.f);
return VectorLerpPiecewise(From, To,
glm::vec3(xAlpha, yAlpha, zAlpha));
}
int float_epsilon(mfloat_t a, mfloat_t b, mfloat_t e)
{
return fabs(a - b) < e;
}
int lerpparam(float offset, float anchorwidth, float floatwidth)
{
return (offset > (anchorwidth + floatwidth)) && !float_epsilon(anchorwidth + floatwidth, offset, 0.5f);
}
mfloat_t *vec3lerp(mfloat_t from[3], mfloat_t to[3], mfloat_t a[3])
{
from[0] = from[0] + (to[0] - from[0]) * a[0];
from[1] = from[1] + (to[1] - from[1]) * a[1];
from[2] = from[2] + (to[2] - from[2]) * a[2];
}
void follow_calctargets()
{
}
void ThirdPersonFollow::CalculateTargets()
{
// For translation
TargetPosition = CalculatePosition();
// For rotation
// TODO: Check of this implementation is the same as UKismetMath FindLookAtRotation
TargetRotation =
RemoveLockedRotation(glm::quat
(mytransform->
get_global_transform()->get_origin() -
Target->
get_global_transform()->get_origin()));
}
follow_removelockedrot()
{
}
glm::quat ThirdPersonFollow::
RemoveLockedRotation(const glm::quat & CurrentRotation)
{
glm::vec3 NewRotator;
glm::vec3 CurrentRotator = glm::eulerAngles(CurrentRotation);
//
NewRotator.x =
LockRoll ? mytransform->get_rotation().x : CurrentRotator.x;
NewRotator.y =
LockPitch ? mytransform->get_rotation().y : CurrentRotator.y;
NewRotator.z =
LockYaw ? mytransform->get_rotation().z : CurrentRotator.z;
return glm::quat(NewRotator);
}
// The target offset is the value of where the target is compared to where he "should" be based on where
// the camera is. It is what the camera needs to move to be centered on the target
void ThirdPersonFollow::CalculateTargetOffset()
{
glm::vec3 p1 =
(mytransform->Forward() * Distance) +
TFOR.TransformDirection(Offset) +
mytransform->get_global_translation();
glm::vec3 p2 =
TFOR.InverseTransformDirection(Target->get_global_translation());
glm::vec3 p3 = TFOR.InverseTransformDirection(p1);
TargetOffset = p2 - p3;
}
void follow_targetoffset()
{
mfloat_t p1[3], p2[3], p3[3] = {0};
}
*/

16
docs/api/actor.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: "actor"
type: docs
---
# actor
### toString() <sub>function</sub>
### spawn(script, config, callback) <sub>function</sub>
### clear() <sub>function</sub>
### kill() <sub>function</sub>
### delay(fn, seconds) <sub>function</sub>

42
docs/api/console.md Normal file
View File

@@ -0,0 +1,42 @@
---
title: "console"
type: docs
---
# console
The console object provides various logging, debugging, and output methods.
### print() <sub>function</sub>
### spam(msg) <sub>function</sub>
Output a spam-level message for very verbose logging.
### debug(msg) <sub>function</sub>
Output a debug-level message.
### info(msg) <sub>function</sub>
Output info level message.
### warn(msg) <sub>function</sub>
Output warn level message.
### log(msg) <sub>function</sub>
Output directly to in game console.
### error(e) <sub>function</sub>
Output error level message, and print stacktrace.
### panic(e) <sub>function</sub>
Output a panic-level message and exit the program.
### assert(op, str = `assertion failed [value '${op}']`) <sub>function</sub>
If the condition is false, print an error and panic.

62
docs/api/index.md Normal file
View File

@@ -0,0 +1,62 @@
---
title: "API Reference"
type: docs
---
# API Reference
Prosperon's API is a set of modules imported with `use()`. There is no global engine object — import only what you need.
## Game-Facing Modules
These are the modules you use to build games:
| Module | Import | Purpose |
|--------|--------|---------|
| **core** | `use('core')` | Main loop, window, GPU initialization |
| **sprite** | `use('sprite')` | Create and manage sprites |
| **compositor** | `use('compositor')` | Build render plans from scene configs |
| **camera** | `use('camera')` | Viewport into the world |
| **input** | `use('input')` | Action mapping, device routing, control stacks |
| **sound** | `use('sound')` | Audio playback and mixing |
| **world** | `use('world')` | Entity management |
| **text2d** | `use('text2d')` | Text rendering |
| **shape2d** | `use('shape2d')` | SDF-rendered shapes (rect, circle, ellipse, pill) |
| **tilemap2d** | `use('tilemap2d')` | Grid-based tile rendering |
| **line2d** | `use('line2d')` | Line rendering |
| **particles2d** | `use('particles2d')` | Particle emitters |
| **tween** | `use('tween')` | Value interpolation over time |
| **ease** | `use('ease')` | Easing functions |
| **color** | `use('color')` | Color utilities |
| **resources** | `use('resources')` | Asset path resolution |
| **action** | `use('action')` | Action definitions |
| **draw2d** | `use('draw2d')` | Immediate-mode 2D drawing |
| **clay** | `use('clay')` | UI layout |
| **point** | `use('point')` | Point/vector utilities |
## Actor Primitives
These are built into the Pit language runtime and available in all `.ce` programs:
| Primitive | Purpose |
|-----------|---------|
| `$start(callback, path)` | Spawn a child actor from a program |
| `$receiver(callback)` | Register a message handler |
| `send(target, message)` | Send a message to an actor |
| `$delay(callback, seconds)` | Schedule a one-shot callback |
| `$clock(callback, interval)` | Schedule a repeating callback |
| `$stop()` | Terminate the current actor |
## Globals
| Name | Purpose |
|------|---------|
| `use(path)` | Import a module |
| `log.console(msg)` | Print to console |
| `meme(proto, data)` | Create object with prototype and overrides |
## Internal Modules
These are used by the engine internally and not intended for direct game use:
`film2d`, `effects`, `sdl_gpu`, `graphics`, `rasterize`, `fx_graph`, `debug_imgui`, `device`, `input/backends/*`, `input/bindings`, `input/devices`, `input/router`

92
docs/api/modules/actor.md Normal file
View File

@@ -0,0 +1,92 @@
---
title: "actor"
type: docs
---
# actor
A set of utilities for iterating over a hierarchy of actor-like objects, as well
as managing tag-based lookups. Objects are assumed to have a "objects" property,
pointing to children or sub-objects, forming a tree.
### all_objects(fn, startobj) <sub>function</sub>
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one.
**fn**: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
**startobj**: The root object at which iteration begins, default is the global "world".
**Returns**: The first truthy value returned by fn, or undefined if none.
### find_object(fn, startobj) <sub>function</sub>
Intended to find a matching object within the hierarchy.
**fn**: A callback or criteria to locate a particular object.
**startobj**: The root object at which search begins, default "world".
**Returns**: Not yet implemented.
### tag_add(tag, obj) <sub>function</sub>
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
**tag**: A string tag to associate with the object.
**obj**: The object to add under this tag.
**Returns**: None
### tag_rm(tag, obj) <sub>function</sub>
Remove the given object from the specified tags set, if it exists.
**tag**: The tag to remove the object from.
**obj**: The object to remove from the tag set.
**Returns**: None
### tag_clear_guid(obj) <sub>function</sub>
Remove the object from all tag sets.
**obj**: The object whose tags should be cleared.
**Returns**: None
### objects_with_tag(tag) <sub>function</sub>
Retrieve all objects currently tagged with the specified tag.
**tag**: A string tag to look up.
**Returns**: An array of objects associated with the given tag.

View File

@@ -0,0 +1,51 @@
---
title: "camera"
type: docs
---
# camera
### list() <sub>function</sub>
Return an array of available camera device IDs.
**Returns**: An array of camera IDs, or undefined if no cameras are available.
### open(id) <sub>function</sub>
Open a camera device with the given ID.
**id**: The camera ID to open.
**Returns**: A camera object on success, or throws an error if the camera cannot be opened.
### name(id) <sub>function</sub>
Return the name of the camera with the given ID.
**id**: The camera ID to query.
**Returns**: A string with the camera's name, or throws an error if the name cannot be retrieved.
### position(id) <sub>function</sub>
Return the physical position of the camera with the given ID.
**id**: The camera ID to query.
**Returns**: A string indicating the camera position ("unknown", "front", or "back").

12
docs/api/modules/cmd.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "cmd"
type: docs
---
# cmd
### length <sub>number</sub>
### name <sub>string</sub>
### prototype <sub>object</sub>

12
docs/api/modules/color.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "color"
type: docs
---
# color
### Color <sub>object</sub>
### esc <sub>object</sub>
### ColorMap <sub>object</sub>

81
docs/api/modules/debug.md Normal file
View File

@@ -0,0 +1,81 @@
---
title: "debug"
type: docs
---
# debug
### stack_depth() <sub>function</sub>
Return the current stack depth.
**Returns**: A number representing the stack depth.
### build_backtrace() <sub>function</sub>
Build and return a backtrace of the current call stack.
**Returns**: An object representing the call stack backtrace.
### closure_vars(fn) <sub>function</sub>
Return the closure variables for a given function.
**fn**: The function object to inspect.
**Returns**: An object containing the closure variables.
### local_vars(depth) <sub>function</sub>
Return the local variables for a specific stack frame.
**depth**: The stack frame depth to inspect.
**Returns**: An object containing the local variables at the specified depth.
### fn_info(fn) <sub>function</sub>
Return metadata about a given function.
**fn**: The function object to inspect.
**Returns**: An object with metadata about the function.
### backtrace_fns() <sub>function</sub>
Return an array of functions in the current backtrace.
**Returns**: An array of function objects from the call stack.
### dump_obj(obj) <sub>function</sub>
Return a string representation of a given object.
**obj**: The object to dump.
**Returns**: A string describing the object's contents.

44
docs/api/modules/dmon.md Normal file
View File

@@ -0,0 +1,44 @@
---
title: "dmon"
type: docs
---
# dmon
### watch() <sub>function</sub>
Start watching the root directory, recursively.
This function begins monitoring the specified directory and its subdirectories recursively for events such as file creation, deletion, modification, or movement. Events are queued and can be retrieved by calling poll.
:throws: An error if dmon is already watching.
**Returns**: None
### unwatch() <sub>function</sub>
Stop watching the currently monitored directory.
This function halts filesystem monitoring for the directory previously set by watch. It clears the watch state, allowing a new watch to be started.
:throws: An error if no directory is currently being watched.
**Returns**: None
### poll(callback) <sub>function</sub>
Retrieve and process queued filesystem events.
This function dequeues all pending filesystem events and invokes the provided callback for each one. The callback receives an event object with properties: 'action' (string: "create", "delete", "modify", or "move"), 'root' (string: watched directory), 'file' (string: affected file path), and 'old' (string: previous file path for move events, empty if not applicable).
**callback**: A function to call for each event, receiving an event object as its argument.
**Returns**: None

43
docs/api/modules/doc.md Normal file
View File

@@ -0,0 +1,43 @@
---
title: "doc"
type: docs
---
# doc
Provides a consistent way to create documentation for prosperon elements. Objects are documented by adding docstrings directly to object-like things (functions, objects, ...), or to an object's own "doc object".
Docstrings are set to the symbol `cell.DOC`
```js
// Suppose we have a module that returns a function
function greet(name) { log.console("Hello, " + name) }
// We can attach a docstring
greet.doc = `
Greets the user by name.
`
// A single function is a valid return!
return greet
```
```js
// Another way is to add a docstring object to an object
var greet = {
hello() { log.console('hello!') }
}
greet[cell.DOC] = {}
greet[cell.DOC][cell.DOC] = 'An object full of different greeter functions'
greet[cell.DOC].hello = 'A greeter that says, "hello!"'
```
**name**: The name of the person to greet.
### writeDocFile(obj, title) <sub>function</sub>
Return a markdown string for a given obj, with an optional title.

View File

@@ -0,0 +1,95 @@
---
title: "draw2d"
type: docs
---
# draw2d
A collection of retained-mode 2D drawing factories. Each factory creates an object that auto-registers with `film2d` and renders via the compositor. Destroy objects when no longer needed.
```javascript
var draw = use('draw2d')
```
## Factories
### draw.sprite(props)
Create a sprite from an image. Auto-registers with `film2d`.
```javascript
var s = draw.sprite({
image: "hero.png",
pos: {x: 100, y: 200},
width: 32, height: 32,
plane: 'game', layer: 0
})
s.destroy() // remove from renderer
```
See `sprite.cm` for full property list: `pos`, `image`, `width`, `height`, `anchor_x`, `anchor_y`, `rotation`, `color`, `opacity`, `tint`, `filter`, `plane`, `layer`, `groups`, `visible`, `flip`, `fit`, `uv`.
### draw.shape.rect(props) / circle(props) / ellipse(props) / pill(props)
Create SDF shapes. Supports fill, stroke, rounded corners, dashing, feathering, and texture fill.
```javascript
var box = draw.shape.rect({
pos: {x: 50, y: 50}, width: 100, height: 60,
fill: {r: 1, g: 0, b: 0, a: 1},
stroke: {r: 1, g: 1, b: 1, a: 1}, stroke_thickness: 2,
radius: 8,
plane: 'game'
})
var ball = draw.shape.circle({
pos: {x: 200, y: 200}, radius: 16,
fill: {r: 0, g: 1, b: 0, a: 1},
plane: 'game', groups: ['glow']
})
```
Properties: `shape_type`, `pos`, `width`, `height`, `radius`, `corner_style`, `feather`, `stroke_thickness`, `stroke_align`, `dash_len`, `gap_len`, `dash_offset`, `cap`, `join`, `fill`, `stroke`, `blend`, `opacity`, `fill_tex`, `uv`, `plane`, `layer`, `groups`, `visible`.
### draw.text(props)
Create a text label. Updates live when you change `.text`.
```javascript
var label = draw.text({
text: "Score: 0",
pos: {x: 10, y: 500},
font: "fonts/dos", size: 16,
color: {r: 1, g: 1, b: 1, a: 1},
plane: 'hud'
})
label.text = "Score: 42" // updates on next frame
```
### draw.tilemap(props)
Create a tile-based map. See `tilemap2d.cm`.
### draw.anim(props)
Create an animated sprite from an aseprite/gif file. Auto-plays if the image has frames.
```javascript
var anim = draw.anim({
image: "hero.aseprite",
pos: {x: 100, y: 100},
plane: 'game'
})
anim.play("walk") // play named animation
anim.stop() // pause
anim.resume() // resume
anim.update(dt) // advance frame (call from update loop)
```
## Lifecycle
All draw2d objects register with `film2d` on creation. Call `.destroy()` to unregister and remove from rendering. Set `.visible = false` to hide without destroying.
## Planes and Groups
Every drawable has a `plane` (string) and optional `groups` (array of strings). The compositor renders drawables per-plane. Groups route drawables through effects (bloom, mask, etc.).

50
docs/api/modules/enet.md Normal file
View File

@@ -0,0 +1,50 @@
---
title: "enet"
type: docs
---
# enet
### initialize() <sub>function</sub>
Initialize the ENet library. Must be called before using any ENet functionality.
Throws an error if initialization fails.
**Returns**: None
### deinitialize() <sub>function</sub>
Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
need any ENet functionality.
**Returns**: None
### create_host(address) <sub>function</sub>
Create an ENet host for either a client-like unbound host or a server bound to a specific
address and port:
- If no argument is provided, creates an unbound "client-like" host with default settings
(maximum 32 peers, 2 channels, unlimited bandwidth).
- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
Throws an error if host creation fails for any reason.
omit to create an unbound client-like host.
**address**: (optional) A string in 'ip:port' format to bind the host (server), or
**Returns**: An ENetHost object.

30
docs/api/modules/event.md Normal file
View File

@@ -0,0 +1,30 @@
---
title: "event"
type: docs
---
# event
### push_event(event) <sub>function</sub>
Push a custom user event into SDL's queue, passing a callback function.
**event**: A function to call when this event is consumed.
**Returns**: None
### engine_input(callback) <sub>function</sub>
Poll all system events (keyboard, mouse, etc.) and call the given function with each event object.
**callback**: A function that executes on each event consumed from the poll.
**Returns**: None

View File

@@ -0,0 +1,226 @@
---
title: "geometry"
type: docs
---
# geometry
A collection of geometry-related functions for circles, spheres, boxes, polygons,
and rectangle utilities. Some functionality is implemented in C and exposed here.
### rect_intersection(a, b) <sub>function</sub>
Return the intersection of two rectangles. The result may be empty if no intersection.
**a**: The first rectangle as {x, y, w, h}.
**b**: The second rectangle as {x, y, w, h}.
**Returns**: A rectangle that is the intersection of the two. May have zero width/height if no overlap.
### rect_intersects(a, b) <sub>function</sub>
**a**: Rectangle {x,y,w,h}.
**b**: Rectangle {x,y,w,h}.
**Returns**: A boolean indicating whether the two rectangles overlap.
### rect_expand(a, b) <sub>function</sub>
Merge or combine two rectangles, returning their bounding rectangle.
**a**: Rectangle {x,y,w,h}.
**b**: Rectangle {x,y,w,h}.
**Returns**: A new rectangle that covers the bounds of both input rectangles.
### rect_inside(inner, outer) <sub>function</sub>
**inner**: A rectangle to test.
**outer**: A rectangle that may contain 'inner'.
**Returns**: True if 'inner' is completely inside 'outer', otherwise false.
### rect_random(rect) <sub>function</sub>
**rect**: A rectangle {x,y,w,h}.
**Returns**: A random point within the rectangle (uniform distribution).
### cwh2rect(center, wh) <sub>function</sub>
Helper: convert a center point and width/height vector to a rect object.
**center**: A 2D point [cx, cy].
**wh**: A 2D size [width, height].
**Returns**: A rectangle {x, y, w, h} with x,y set to center and w,h set to the given size.
### rect_point_inside(rect, point) <sub>function</sub>
**rect**: A rectangle {x,y,w,h}.
**point**: A 2D point [px, py].
**Returns**: True if the point lies inside the rectangle, otherwise false.
### rect_pos(rect) <sub>function</sub>
**rect**: A rectangle {x,y,w,h}.
**Returns**: A 2D vector [x,y] giving the rectangle's position.
### rect_move(rect, offset) <sub>function</sub>
**rect**: A rectangle {x,y,w,h}.
**offset**: A 2D vector to add to the rectangle's position.
**Returns**: A new rectangle with updated x,y offset.
### box(w, h) <sub>function</sub>
Construct a box centered at the origin with the given width and height. This overrides the box object above.
**w**: The width of the box.
**h**: The height of the box.
**Returns**: An array of four 2D points representing the corners of a rectangle centered at [0,0].
### sphere <sub>object</sub>
Sphere-related geometry functions:
- volume(r): Return the volume of a sphere with radius r.
- random(r, theta, phi): Return a random point on or inside a sphere.
### circle <sub>object</sub>
Circle-related geometry functions:
- area(r): Return the area of a circle with radius r.
- random(r, theta): Return a random 2D point on a circle; uses sphere.random internally and extracts x,z.
### ngon(radius, n) <sub>function</sub>
Generates a regular n-gon by calling geometry.arc with full 360 degrees.
**radius**: The radius of the n-gon from center to each vertex.
**n**: Number of sides/vertices.
**Returns**: An array of 2D points forming a regular n-gon.
### arc(radius, angle, n, start) <sub>function</sub>
Generate an arc (or partial circle) of n points, each angle spread equally over 'angle' degrees from 'start'.
**radius**: The distance from center to the arc points.
**angle**: The total angle (in degrees) over which points are generated, capped at 360.
**n**: Number of segments (if <=1, empty array is returned).
**start**: Starting angle (in degrees), default 0.
**Returns**: An array of 2D points along the arc.
### corners2points(ll, ur) <sub>function</sub>
Similar to box.points, but calculates differently.
**ll**: Lower-left 2D coordinate.
**ur**: Upper-right 2D coordinate (relative offset in x,y).
**Returns**: A four-point array of corners [ll, lower-right, upper-right, upper-left].
### sortpointsccw(points) <sub>function</sub>
Sort an array of points in CCW order based on their angles from the centroid.
**points**: An array of 2D points.
**Returns**: A new array of the same points, sorted counterclockwise around their centroid.
### points2cm(points) <sub>function</sub>
**points**: An array of 2D points.
**Returns**: The centroid (average x,y) of the given points.

View File

@@ -0,0 +1,283 @@
---
title: "graphics"
type: docs
---
# graphics
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.
### make_sprite_mesh(sprites) <sub>function</sub>
:param oldMesh (optional): An existing mesh object to reuse/resize if possible.
Given an array of sprites, build a single geometry mesh for rendering them.
**sprites**: An array of sprite objects, each containing .rect (or transform), .src (UV region), .color, etc.
**Returns**: A GPU mesh object with pos, uv, color, and indices buffers for all sprites.
### make_sprite_queue(sprites, camera, pipeline, sort) <sub>function</sub>
Given an array of sprites, optionally sort them, then build a queue of pipeline commands.
Each group with a shared image becomes one command.
**sprites**: An array of sprite objects.
**camera**: (unused in the C code example) Typically a camera or transform for sorting?
**pipeline**: A pipeline object for rendering.
**sort**: An integer or boolean for whether to sort sprites; if truthy, sorts by layer & texture.
**Returns**: An array of pipeline commands: geometry with mesh references, grouped by image.
### make_text_buffer(text, rect, angle, color, wrap, font) <sub>function</sub>
Generate a GPU buffer mesh of text quads for rendering with a font, etc.
**text**: The string to render.
**rect**: A rectangle specifying position and possibly wrapping.
**angle**: Rotation angle (unused or optional).
**color**: A color for the text (could be a vec4).
**wrap**: The width in pixels to wrap text, or 0 for no wrap.
**font**: A font object created by graphics.make_font or graphics.get_font.
**Returns**: A geometry buffer mesh (pos, uv, color, indices) for rendering text.
### rectpack(width, height, sizes) <sub>function</sub>
Perform a rectangle packing using the stbrp library. Return positions for each rect.
**width**: The width of the area to pack into.
**height**: The height of the area to pack into.
**sizes**: An array of [w,h] pairs for the rectangles to pack.
**Returns**: An array of [x,y] coordinates placing each rect, or null if they don't fit.
### make_rtree() <sub>function</sub>
Create a new R-Tree for geometry queries.
**Returns**: An R-Tree object for quickly querying many rectangles or sprite bounds.
### make_texture(data) <sub>function</sub>
Convert raw image bytes into an SDL_Surface object.
**data**: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer.
**Returns**: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering.
### make_gif(data) <sub>function</sub>
Load a GIF, returning its frames. If it's a single-frame GIF, the result may have .surface only.
**data**: An ArrayBuffer containing GIF data.
**Returns**: An object with frames[], each frame having its own .surface. Some also have a .texture for GPU use.
### make_aseprite(data) <sub>function</sub>
Load an Aseprite/ASE file from an array of bytes, returning frames or animations.
**data**: An ArrayBuffer containing Aseprite (ASE) file data.
**Returns**: An object containing frames or animations, each with .surface. May also have top-level .surface for a single-layer case.
### cull_sprites(sprites, camera) <sub>function</sub>
Filter an array of sprites to only those visible in the provided cameras view.
**sprites**: An array of sprite objects (each has rect or transform).
**camera**: A camera or bounding rectangle defining the view area.
**Returns**: A new array of sprites that are visible in the camera's view.
### rects_to_sprites(rects, image) <sub>function</sub>
Convert an array of rect coords into sprite objects referencing a single image.
**rects**: An array of rect coords or objects.
**image**: An image object (with .texture).
**Returns**: An array of sprite objects referencing the 'image' and each rect for UV or position.
### make_surface(dimensions) <sub>function</sub>
Create a blank surface in RAM.
**dimensions**: The size object {width, height}, or an array [w,h].
**Returns**: A blank RGBA surface with the given dimensions, typically for software rendering or icons.
### make_cursor(opts) <sub>function</sub>
**opts**: An object with {surface, hotx, hoty} or similar.
**Returns**: An SDL_Cursor object referencing the given surface for a custom mouse cursor.
### make_font(data, size) <sub>function</sub>
Load a font from TTF/OTF data at the given size.
**data**: TTF/OTF file data as an ArrayBuffer.
**size**: Pixel size for rendering glyphs.
**Returns**: A font object with surface, texture, and glyph data, for text rendering with make_text_buffer.
### make_sprite() <sub>function</sub>
Create a new sprite object, storing default properties.
**Returns**: A new sprite object, which typically has .rect, .color, .layer, .image, etc.
### make_line_prim(points, thickness, startCap, endCap, color) <sub>function</sub>
Build a GPU mesh representing a thick polyline from an array of points, using parsl or a similar library under the hood.
**points**: An array of [x,y] points forming the line.
**thickness**: The thickness (width) of the polyline.
**startCap**: (Unused) Possibly the type of cap for the start.
**endCap**: (Unused) Possibly the type of cap for the end.
**color**: A color to apply to the line.
**Returns**: A geometry mesh object suitable for rendering the line via a pipeline command.
### is_image(obj) <sub>function</sub>
**obj**: An object to check.
**Returns**: True if 'obj' has a .texture and a .rect property, indicating it's an image object.
### texture(path) <sub>function</sub>
Load or retrieve a cached image, converting it into a GPU texture. If 'path' is already an object, its returned directly.
**path**: A string path to an image file or an already-loaded image object.
**Returns**: An image object with {surface, texture, frames?, etc.} depending on the format.
### tex_hotreload(file) <sub>function</sub>
Reload the image for the given file, updating the cached copy in memory and GPU.
**file**: The file path that was changed on disk.
**Returns**: None
### get_font(path, size) <sub>function</sub>
Load a font from file if not cached, or retrieve from cache if already loaded.
**path**: A string path to a font file, optionally with ".size" appended.
**size**: Pixel size of the font, if not included in 'path'.
**Returns**: A font object with .surface and .texture for rendering text.
### queue_sprite_mesh(queue) <sub>function</sub>
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.
**queue**: An array of draw commands, some of which are {type:'sprite'} objects.
**Returns**: An array of references to GPU buffers [pos,uv,color,indices].

1249
docs/api/modules/imgui.md Normal file

File diff suppressed because it is too large Load Diff

73
docs/api/modules/input.md Normal file
View File

@@ -0,0 +1,73 @@
---
title: "input"
type: docs
---
# input
### mouse_show(show) <sub>function</sub>
Show or hide the mouse cursor. Pass true to show, false to hide.
**show**: Boolean. True to show, false to hide.
**Returns**: None
### mouse_lock(lock) <sub>function</sub>
Capture or release the mouse, confining it within the window if locked.
**lock**: Boolean. True to lock, false to unlock.
**Returns**: None
### cursor_set(cursor) <sub>function</sub>
Set the given cursor (created by os.make_cursor) as the active mouse cursor.
**cursor**: The cursor to set.
**Returns**: None
### keyname(keycode) <sub>function</sub>
Given a numeric keycode, return the corresponding key name (e.g., from SDL).
**keycode**: A numeric SDL keycode.
**Returns**: A string with the key name.
### keymod() <sub>function</sub>
Return an object describing the current modifier keys, e.g. {shift:true, ctrl:true}.
**Returns**: An object with boolean fields for each modifier key.
### mousestate() <sub>function</sub>
Return an object describing the current mouse state, including x,y coordinates
and booleans for pressed buttons (left, middle, right, x1, x2).
**Returns**: Object { x, y, left, middle, right, x1, x2 }

248
docs/api/modules/io.md Normal file
View File

@@ -0,0 +1,248 @@
---
title: "io"
type: docs
---
# io
### rm(path) <sub>function</sub>
Remove the file or empty directory at the given path.
**path**: The file or empty directory to remove. Must be empty if a directory.
**Returns**: None
### mkdir(path) <sub>function</sub>
Create a directory at the given path.
**path**: The directory path to create.
**Returns**: None
### stat(path) <sub>function</sub>
Return an object describing file metadata for the given path. The object includes
filesize, modtime, createtime, and accesstime. Throw an error if the path does not exist.
**path**: The file or directory to retrieve metadata for.
**Returns**: An object with metadata (filesize, modtime, createtime, accesstime).
### globfs(patterns) <sub>function</sub>
Return an array of files that do not match any of the provided glob patterns. It
recursively enumerates the filesystem within PHYSFS. Each pattern is treated as an
"ignore" rule, similar to .gitignore usage.
**patterns**: An array of glob patterns to ignore. Any file matching one of these is skipped.
**Returns**: An array of matching file paths.
### match(pattern, string) <sub>function</sub>
Return boolean indicating whether the given wildcard pattern matches the provided
string. Dots must match dots. Case is not ignored.
Patterns can incorporate:
'?' - Matches exactly one character (except leading dots or slashes).
'*' - Matches zero or more characters (excluding path separators).
'**' - Matches zero or more characters, including path separators.
'[abc]' - A bracket expression; matches any single character from the set. Ranges like [a-z], [0-9] also work.
'[[:alpha:]]' - POSIX character classes can be used inside brackets.
'\' - Backslash escapes the next character.
'!' - If placed immediately inside brackets (like [!abc]), it negates the set.
**pattern**: The wildcard pattern to compare.
**string**: The string to test against the wildcard pattern.
**Returns**: True if matched, otherwise false.
### exists(path) <sub>function</sub>
Return a boolean indicating whether the file or directory at the given path exists.
**path**: The file or directory path to check.
**Returns**: True if the path exists, otherwise false.
### mount(archiveOrDir, mountPoint) <sub>function</sub>
Mount a directory or archive at the specified mount point. An undefined mount
point mounts to '/'. Throw on error.
**archiveOrDir**: The directory or archive to mount.
**mountPoint**: The path at which to mount. If omitted or undefined, '/' is used.
**Returns**: None
### unmount(path) <sub>function</sub>
Unmount a previously mounted directory or archive. Throw on error.
**path**: The directory or archive mount point to unmount.
**Returns**: None
### slurp(path) <sub>function</sub>
Read the entire file at the given path as a string. Throw on error.
**path**: The file path to read from.
**Returns**: A string with the files contents.
### slurpbytes(path) <sub>function</sub>
Read the entire file at the given path as a raw ArrayBuffer. Throw on error.
**path**: The file path to read from.
**Returns**: An ArrayBuffer containing the files raw bytes.
### slurpwrite(data, path) <sub>function</sub>
Write data (string or ArrayBuffer) to the given file path. Overwrite if it exists.
Throw on error.
**data**: The data to write (string or ArrayBuffer).
**path**: The file path to write to.
**Returns**: None
### writepath(path) <sub>function</sub>
Set the write directory. Subsequent writes will go here by default. Throw on error.
**path**: The directory path to set as writable.
**Returns**: None
### basedir() <sub>function</sub>
Return the application's base directory (where the executable is located).
**Returns**: A string with the base directory path.
### prefdir(org, app) <sub>function</sub>
Get the user-and-app-specific path where files can be written.
**org**: The name of your organization.
**app**: The name of your application.
**Returns**: A string with the user's directory path.
### realdir(path) <sub>function</sub>
Return the actual, real directory (on the host filesystem) that contains the given
file path. Return undefined if not found.
**path**: The file path whose real directory is requested.
**Returns**: A string with the real directory path, or undefined.
### open(path) <sub>function</sub>
Open a file for writing, returning a file object that can be used for further
operations. Throw on error.
**path**: The file path to open for writing.
**Returns**: A file object for subsequent write operations.
### searchpath() <sub>function</sub>
Return an array of all directories in the current paths.
**Returns**: An array of directory paths in the search path.
### enumerate(path, recurse) <sub>function</sub>
Return an array of files within the given directory, optionally recursing into
subdirectories.
**path**: The directory to list.
**recurse**: Whether to recursively include subdirectories (true or false).
**Returns**: An array of file (and directory) paths found.
### mount_core() <sub>function</sub>
### is_directory() <sub>function</sub>

180
docs/api/modules/js.md Normal file
View File

@@ -0,0 +1,180 @@
---
title: "js"
type: docs
---
# js
Provides functions for introspecting and configuring the QuickJS runtime engine.
Includes debug info, memory usage, GC controls, code evaluation, etc.
### cycle_hook(callback) <sub>function</sub>
or undefined to remove the callback.
Register or remove a hook function that QuickJS calls once per execution cycle. If the callback
is set, it receives a single argument (an optional object/value describing the cycle). If callback
is undefined, the hook is removed.
**callback**: A function to call each time QuickJS completes a "cycle" (internal VM loop),
**Returns**: None
### dump_shapes() <sub>function</sub>
Use this for internal debugging of object shapes.
**Returns**: A debug string describing the internal shape hierarchy used by QuickJS.
### dump_atoms() <sub>function</sub>
known by QuickJS. Helpful for diagnosing memory usage or potential key collisions.
**Returns**: A debug string listing all currently registered atoms (internal property keys/symbols)
### dump_class() <sub>function</sub>
Shows how many objects of each class exist, useful for advanced memory or performance profiling.
**Returns**: A debug string describing the distribution of JS object classes in the QuickJS runtime.
### dump_objects() <sub>function</sub>
useful for debugging memory leaks or object lifetimes.
**Returns**: A debug string listing certain internal QuickJS objects and their references,
### dump_type_overheads() <sub>function</sub>
Displays memory usage breakdown for different internal object types.
**Returns**: A debug string describing the overheads for various JS object types in QuickJS.
### stack_info() <sub>function</sub>
Internal debugging utility to examine call stack details.
**Returns**: An object or string describing the runtime's current stack usage and capacity.
### calc_mem(value) <sub>function</sub>
Compute the approximate size of a single JS value in memory. This is a best-effort estimate.
**value**: A JavaScript value to analyze.
**Returns**: Approximate memory usage (in bytes) of that single value.
### mem() <sub>function</sub>
including total allocated bytes, object counts, and more.
Retrieve an overview of the runtimes memory usage.
**Returns**: An object containing a comprehensive snapshot of memory usage for the current QuickJS runtime,
### mem_limit(bytes) <sub>function</sub>
Set the upper memory limit for the QuickJS runtime. Exceeding this limit may cause operations to
fail or throw errors.
**bytes**: The maximum memory (in bytes) QuickJS is allowed to use.
**Returns**: None
### gc_threshold(bytes) <sub>function</sub>
Set the threshold (in bytes) for QuickJS to perform an automatic GC pass when memory usage surpasses it.
**bytes**: The threshold (in bytes) at which the engine triggers automatic garbage collection.
**Returns**: None
### max_stacksize(bytes) <sub>function</sub>
Set the maximum stack size for QuickJS. If exceeded, the runtime may throw a stack overflow error.
**bytes**: The maximum allowed stack size (in bytes) for QuickJS.
**Returns**: None
### memstate() <sub>function</sub>
Gives a quick overview of the memory usage, including malloc size and other allocations.
**Returns**: A simpler memory usage object (malloc sizes, etc.) for the QuickJS runtime.
### gc() <sub>function</sub>
Force an immediate, full garbage collection pass, reclaiming unreachable memory.
**Returns**: None
### eval(src, filename) <sub>function</sub>
Execute a string of JavaScript code in the current QuickJS context.
**src**: A string of JavaScript source code to evaluate.
**filename**: (Optional) A string for the filename or label, used in debugging or stack traces.
**Returns**: The result of evaluating the given source code.

20
docs/api/modules/json.md Normal file
View File

@@ -0,0 +1,20 @@
---
title: "json"
type: docs
---
# json
### encode(val,space,replacer,whitelist) <sub>function</sub>
Produce a JSON text from a Javascript object. If a record value, at any level, contains a json() method, it will be called, and the value it returns (usually a simpler record) will be JSONified.
If the record does not have a json() method, and if whitelist is a record, then only the keys that are associated with true in the whitelist are included.
If the space input is true, then line breaks and extra whitespace will be included in the text.
### decode(text,reviver) <sub>function</sub>
The text text is parsed, and the resulting value (usually a record or an array) is returned.
The optional reviver input is a method that will be called for every key and value at every level of the result. Each value will be replaced by the result of the reviver function. This can be used to reform data-only records into method-bearing records, or to transform date strings into seconds.

8
docs/api/modules/loop.md Normal file
View File

@@ -0,0 +1,8 @@
---
title: "loop"
type: docs
---
# loop
### step() <sub>function</sub>

120
docs/api/modules/math.md Normal file
View File

@@ -0,0 +1,120 @@
---
title: "math"
type: docs
---
# math
### dot() <sub>function</sub>
Compute the dot product between two numeric arrays, returning a scalar. Extra elements are ignored.
### project() <sub>function</sub>
Project one vector onto another, returning a new array of the same dimension.
### rotate() <sub>function</sub>
Rotate a 2D point (or array of length 2) by the given angle (in turns) around an optional pivot.
### midpoint() <sub>function</sub>
Compute the midpoint of two arrays of numbers. Only the first two entries are used if 2D is intended.
### reflect() <sub>function</sub>
Reflect a vector across a plane normal. Both arguments must be numeric arrays.
### distance() <sub>function</sub>
Compute the Euclidean distance between two numeric arrays of matching length.
### direction() <sub>function</sub>
Compute the normalized direction vector from the first array to the second.
### angle() <sub>function</sub>
Given a 2D vector, return its angle from the X-axis in radians or some chosen units.
### norm() <sub>function</sub>
Return a normalized copy of the given numeric array. For 2D/3D/4D or arbitrary length.
### angle_between() <sub>function</sub>
Compute the angle between two vectors (2D/3D/4D).
### lerp() <sub>function</sub>
Linear interpolation between two numbers: lerp(a, b, t).
### gcd() <sub>function</sub>
Compute the greatest common divisor of two integers.
### lcm() <sub>function</sub>
Compute the least common multiple of two integers.
### clamp() <sub>function</sub>
Clamp a number between low and high. clamp(value, low, high).
### angledist() <sub>function</sub>
Compute the signed distance between two angles in 'turn' units, e.g. 0..1 range.
### jitter() <sub>function</sub>
Apply a random +/- percentage noise to a number. Example: jitter(100, 0.05) -> ~95..105.
### mean() <sub>function</sub>
Compute the arithmetic mean of an array of numbers.
### sum() <sub>function</sub>
Sum all elements of an array of numbers.
### sigma() <sub>function</sub>
Compute standard deviation of an array of numbers.
### median() <sub>function</sub>
Compute the median of an array of numbers.
### length() <sub>function</sub>
Return the length of a vector (i.e. sqrt of sum of squares).
### from_to() <sub>function</sub>
Return an array of points from a start to an end, spaced out by a certain distance.
### rand() <sub>function</sub>
Return a random float in [0,1).
### randi() <sub>function</sub>
Return a random 32-bit integer.
### srand() <sub>function</sub>
Seed the random number generator with the given integer, or with current time if none.
### TAU <sub>number</sub>
### deg2rad(deg) <sub>function</sub>
### rad2deg(rad) <sub>function</sub>
### turn2rad(x) <sub>function</sub>
### rad2turn(x) <sub>function</sub>
### turn2deg(x) <sub>function</sub>
### deg2turn(x) <sub>function</sub>

32
docs/api/modules/miniz.md Normal file
View File

@@ -0,0 +1,32 @@
---
title: "miniz"
type: docs
---
# miniz
### read(data) <sub>function</sub>
Create a zip reader from the given ArrayBuffer containing an entire ZIP archive.
Return undefined if the data is invalid.
**data**: An ArrayBuffer with the entire ZIP file.
**Returns**: A 'zip reader' object with methods for reading from the archive (mod, exists, slurp).
### write(path) <sub>function</sub>
Create a zip writer that writes to the specified file path. Overwrites the file if
it already exists. Return undefined on error.
**path**: The file path where the ZIP archive will be written.
**Returns**: A 'zip writer' object with methods for adding files to the archive (add_file).

35
docs/api/modules/nota.md Normal file
View File

@@ -0,0 +1,35 @@
---
title: "nota"
type: docs
---
# nota
### encode(value) <sub>function</sub>
Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
:throws: An error if no argument is provided.
**value**: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
**Returns**: An ArrayBuffer containing the NOTA-encoded data.
### decode(buffer) <sub>function</sub>
Decode a NOTA-encoded ArrayBuffer into a JavaScript value.
This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
**buffer**: An ArrayBuffer containing NOTA-encoded data to decode.
**Returns**: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.

106
docs/api/modules/os.md Normal file
View File

@@ -0,0 +1,106 @@
---
title: "os"
type: docs
---
# os
### make_transform() <sub>function</sub>
Create a new transform object that can be used for 2D/3D positioning, scaling, and rotation.
### clean_transforms() <sub>function</sub>
Force an update on all transforms to remove dangling references or perform house-keeping.
### platform() <sub>function</sub>
Return a string with the underlying platform name, like 'Windows', 'Linux', or 'macOS'.
### arch() <sub>function</sub>
Return the CPU architecture string for this system (e.g. 'x64', 'arm64').
### totalmem() <sub>function</sub>
Return the total system RAM in bytes.
### freemem() <sub>function</sub>
Return the amount of free system RAM in bytes, if known.
### hostname() <sub>function</sub>
Return the system's hostname, or an empty string if not available.
### version() <sub>function</sub>
Return the OS or kernel version string, if the platform provides it.
### kill() <sub>function</sub>
Send a signal (e.g., 'SIGINT', 'SIGTERM', etc.) to the current process.
### exit() <sub>function</sub>
Exit the application with the specified exit code.
### now() <sub>function</sub>
Return current time (in seconds as a float) with high resolution.
### openurl() <sub>function</sub>
Open the provided URL in the default web browser, if possible.
### make_timer() <sub>function</sub>
Create a new timer object that will call a specified function after a certain delay.
### update_timers() <sub>function</sub>
Advance all timers by the provided time delta (in seconds).
### sleep() <sub>function</sub>
Block execution for the specified number of seconds.
### battery_pct() <sub>function</sub>
Return the battery level (percentage) or negative if unknown.
### battery_voltage() <sub>function</sub>
Return the current battery voltage in volts, if available.
### battery_seconds() <sub>function</sub>
Return the estimated remaining battery time in seconds, or negative if unknown.
### power_state() <sub>function</sub>
Return a string describing power status: 'on battery', 'charging', 'charged', etc.
### on() <sub>function</sub>
Register a global callback for certain engine-wide or system-level events.
### rt_info() <sub>function</sub>
Return internal QuickJS runtime info, such as object counts.
### rusage() <sub>function</sub>
Return resource usage stats for this process, if the platform supports it.
### mallinfo() <sub>function</sub>
Return detailed memory allocation info (arena size, free blocks, etc.) on some platforms.
### env() <sub>function</sub>
Fetch the value of a given environment variable, or undefined if it doesn't exist.
### system() <sub>function</sub>
Execute a shell command using the system() call. Returns the command's exit code.

View File

@@ -0,0 +1,49 @@
---
title: "packer"
type: docs
---
# packer
### getAllFiles(dir) <sub>function</sub>
Return a list of all files in the given directory that are not matched by .prosperonignore,
skipping directories.
**dir**: The directory to search.
**Returns**: An array of file paths found.
### gatherStats(filePaths) <sub>function</sub>
Analyze a list of files and categorize them as modules, programs, images, or other.
**filePaths**: An array of file paths to analyze.
**Returns**: An object { modules, programs, images, other, total } with counts.
### pack(dir, outPath) <sub>function</sub>
Create a ZIP archive of all files (skipping those matched by .prosperonignore) in the
specified directory and write it to outPath. This uses the miniz module.
**dir**: The directory to zip.
**outPath**: The path (including filename) for the resulting ZIP file.
**Returns**: None (synchronous). Throws an Error if the directory does not exist.

101
docs/api/modules/render.md Normal file
View File

@@ -0,0 +1,101 @@
---
title: "render"
type: docs
---
# render
### _main <sub>object</sub>
A handle for low-level GPU operations via SDL GPU. Freed on GC.
### device <sub>object</sub>
### stencil_writer(...args) <sub>function</sub>
### fillmask(ref) <sub>function</sub>
Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference.
**ref**: The stencil reference value to write.
**Returns**: None
### mask(image, pos, scale, rotation, ref) <sub>function</sub>
Draw an image to the stencil buffer, marking its area with a specified reference value.
**image**: A texture or string path (which is converted to a texture).
**pos**: The translation (x, y) for the image placement.
**scale**: Optional scaling applied to the texture.
**rotation**: Optional rotation in radians (unused by default).
**ref**: The stencil reference value to write.
**Returns**: None
### viewport(rect) <sub>function</sub>
Set the GPU viewport to the specified rectangle.
**rect**: A rectangle [x, y, width, height].
**Returns**: None
### scissor(rect) <sub>function</sub>
Set the GPU scissor region to the specified rectangle (alias of render.viewport).
**rect**: A rectangle [x, y, width, height].
**Returns**: None
### queue(cmd) <sub>function</sub>
Enqueue one or more draw commands. These commands are batched until render_camera is called.
**cmd**: Either a single command object or an array of command objects.
**Returns**: None
### setup_draw() <sub>function</sub>
Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined.
**Returns**: None
### setup_hud() <sub>function</sub>
Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined.
**Returns**: None

View File

@@ -0,0 +1,73 @@
---
title: "resources"
type: docs
---
# resources
### scripts <sub>object</sub>
### images <sub>object</sub>
### sounds <sub>object</sub>
### fonts <sub>object</sub>
### lib <sub>object</sub>
### canonical(file) <sub>function</sub>
### find_image(...args) <sub>function</sub>
### find_sound(...args) <sub>function</sub>
### find_script(...args) <sub>function</sub>
### find_font(...args) <sub>function</sub>
### getAllFiles(dir) <sub>function</sub>
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.
**dir**: The directory to search.
**Returns**: An array of recognized file paths.
### gatherStats(filePaths) <sub>function</sub>
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.
**filePaths**: An array of file paths to analyze.
**Returns**: { scripts, images, sounds, fonts, lib, other, total }
### pack(dir, outPath) <sub>function</sub>
Create a ZIP archive of all recognized files (skipping those matched by .prosperonignore)
in the specified directory and write it to outPath. Recognized extensions are scripts,
images, sounds, fonts, or libs.
:raises Error: If the directory does not exist.
**dir**: The directory to zip.
**outPath**: The path (including filename) for the resulting ZIP file.
**Returns**: None

16
docs/api/modules/sound.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: "sound"
type: docs
---
# sound
### undefined <sub>string</sub>
### pcm(file) <sub>function</sub>
### play(file) <sub>function</sub>
### cry(file) <sub>function</sub>
### music(file, fade = 0.5) <sub>function</sub>

View File

@@ -0,0 +1,14 @@
---
title: "spline"
type: docs
---
# spline
### catmull() <sub>function</sub>
Perform Catmull-Rom spline sampling on an array of 2D points, returning an array of samples.
### bezier() <sub>function</sub>
Perform a Bezier spline (or catmull) sampling on 2D points, returning an array of sampled points.

108
docs/api/modules/time.md Normal file
View File

@@ -0,0 +1,108 @@
---
title: "time"
type: docs
---
# time
The main time object, handling date/time utilities in earth-seconds.
### now() <sub>function</sub>
Return the current system time in seconds (implemented in C extension).
### computer_dst() <sub>function</sub>
Return true if local system time is currently in DST (implemented in C extension).
### computer_zone() <sub>function</sub>
Return local time zone offset from UTC in hours (implemented in C extension).
### second <sub>number</sub>
Number of seconds in a (real) second (always 1).
### minute <sub>number</sub>
Number of seconds in a minute (60).
### hour <sub>number</sub>
Number of seconds in an hour (3600).
### day <sub>number</sub>
Number of seconds in a day (86400).
### week <sub>number</sub>
Number of seconds in a week (604800).
### weekdays <sub>object</sub>
Names of the days of the week, Sunday through Saturday.
### monthstr <sub>object</sub>
Full names of the months of the year, January through December.
### epoch <sub>number</sub>
Base epoch year, from which day 0 is calculated (default 1970).
### hour2minute() <sub>function</sub>
Return the ratio of hour to minute in seconds, e.g. 3600 / 60 => 60.
### day2hour() <sub>function</sub>
Return the ratio of day to hour in seconds, e.g. 86400 / 3600 => 24.
### minute2second() <sub>function</sub>
Return the ratio of minute to second in seconds, e.g. 60 / 1 => 60.
### week2day() <sub>function</sub>
Return the ratio of week to day in seconds, e.g. 604800 / 86400 => 7.
### strparse <sub>object</sub>
Mapping of format tokens (yyyy, mm, dd, etc.) to time fields (year, month, day...).
### isleap(year) <sub>function</sub>
Return true if a given year is leap, based on whether it has 366 days.
### yearsize(y) <sub>function</sub>
Given a year, return 365 or 366 depending on leap-year rules.
### timecode(t, fps = 24) <sub>function</sub>
Convert seconds into a "S:frames" timecode string, with optional FPS (default 24).
### monthdays <sub>object</sub>
An array of days in each month for a non-leap year.
### zones <sub>object</sub>
Table of recognized time zone abbreviations, with offsets (e.g., "-12" -> "IDLW").
### record(num, zone = this.computer_zone() <sub>function</sub>
Convert a timestamp (in seconds) into a record with fields like day, month, year, etc.
### number(rec) <sub>function</sub>
Convert a record back into a numeric timestamp (seconds).
### fmt <sub>string</sub>
Default format string for time.text(), containing tokens like 'yyyy', 'dd', 'hh', etc.
### text(num, fmt = this.fmt, zone) <sub>function</sub>
Format a numeric or record time into a string using a format pattern, e.g. 'hh:nn:ss'.

63
docs/api/modules/tween.md Normal file
View File

@@ -0,0 +1,63 @@
---
title: "tween"
type: docs
---
# tween
### Tween <sub>object</sub>
An object providing methods to create and control tweens with additional features
like looping, custom easing, multiple stages, etc.
Properties:
- default: A template object with loop/time/ease/whole/cb properties.
Methods:
- start(obj, target, tvals, options): Create a tween over multiple target values.
- make: Alias of start.
### Ease <sub>object</sub>
This object provides multiple easing functions that remap a 0..1 input to produce
a smoothed or non-linear output. They can be used standalone or inside tweens.
Available functions:
- linear(t)
- in(t), out(t), inout(t)
- quad.in, quad.out, quad.inout
- cubic.in, cubic.out, cubic.inout
- quart.in, quart.out, quart.inout
- quint.in, quint.out, quint.inout
- expo.in, expo.out, expo.inout
- bounce.in, bounce.out, bounce.inout
- sine.in, sine.out, sine.inout
- elastic.in, elastic.out, elastic.inout
All easing functions expect t in [0..1] and return a remapped value in [0..1].
### tween(from, to, time, fn, cb) <sub>function</sub>
Creates a simple tween that linearly interpolates from "from" to "to" over "time"
and calls "fn" with each interpolated value. Once finished, "fn" is called with "to",
then "cb" is invoked if provided, and the tween is cleaned up.
**from**: The starting object or value to interpolate from.
**to**: The ending object or value to interpolate to.
**time**: The total duration of the tween in milliseconds or some time unit.
**fn**: A callback function that receives the interpolated value at each update.
**cb**: (Optional) A callback invoked once the tween completes.
**Returns**: A function that, when called, cleans up and stops the tween.

197
docs/api/modules/util.md Normal file
View File

@@ -0,0 +1,197 @@
---
title: "util"
type: docs
---
# util
A collection of general-purpose utility functions for object manipulation, merging,
deep copying, safe property access, etc.
### guid() <sub>function</sub>
Return a random 32-character hexadecimal UUID-like string (not guaranteed RFC4122-compliant).
**Returns**: A random 32-character string (hex).
### insertion_sort(arr, cmp) <sub>function</sub>
In-place insertion sort of an array using cmp(a,b)->Number for ordering.
**arr**: The array to be sorted in-place.
**cmp**: Comparison function cmp(a,b)->Number.
**Returns**: The same array, sorted in-place.
### deepfreeze(obj) <sub>function</sub>
Recursively freeze an object and all of its nested objects so they cannot be modified.
**obj**: The object to recursively freeze.
**Returns**: None
### dainty_assign(target, source) <sub>function</sub>
Copy non-function properties from source into matching keys of target without overwriting
keys that don't exist in target. Arrays are deep-copied, and objects are recursively assigned.
**target**: The target object whose keys may be updated.
**source**: The source object containing new values.
**Returns**: None
### get(obj, path, defValue) <sub>function</sub>
Safely retrieve a nested property from obj at path (array or dot-string).
Returns defValue if the property is undefined.
**obj**: The object to traverse.
**path**: A string like "a.b.c" or an array of path segments.
**defValue**: The default value if the property is undefined.
**Returns**: The nested property or defValue.
### isEmpty(o) <sub>function</sub>
Return true if the object has no own properties, otherwise false.
**o**: The object to check.
**Returns**: Boolean indicating if the object is empty.
### dig(obj, path, def) <sub>function</sub>
Ensure a nested path of objects exists inside obj; create objects if missing, and set
the final path component to def.
**obj**: The root object to modify.
**path**: A dot-string specifying nested objects to create.
**def**: The value to store in the final path component, default {}.
**Returns**: The assigned final value.
### access(obj, name) <sub>function</sub>
Traverse obj by dot-separated path name, returning the final value or undefined
if any step is missing.
**obj**: The object to traverse.
**name**: A dot-string path (e.g. "foo.bar.baz").
**Returns**: The value at that path, or undefined if missing.
### mergekey(o1, o2, k) <sub>function</sub>
Helper for merge, updating key k from o2 into o1. Arrays are deep-copied and objects are
recursively merged.
**o1**: The target object.
**o2**: The source object.
**k**: The key to merge.
**Returns**: None
### merge(target, objs) <sub>function</sub>
Merge all passed objects into target, copying or merging each key as needed.
Arrays are deep-copied, objects are recursively merged, etc.
**target**: The target object.
**objs**: One or more objects to merge into target.
**Returns**: The updated target object.
### copy(proto, objs) <sub>function</sub>
Create a new object with proto as its prototype, then mix in additional objects properties.
**proto**: The prototype object for the new object.
**objs**: One or more objects whose properties will be mixed in.
**Returns**: The newly created object.
### obj_lerp(a, b, t) <sub>function</sub>
Linearly interpolate between two objects a and b by factor t, assuming each property
supports .lerp().
**a**: The start object (its properties must have .lerp()).
**b**: The end object (matching properties).
**t**: Interpolation factor (0..1).
**Returns**: A new object with interpolated properties.
### normalizeSpacing(spacing) <sub>function</sub>
Normalize any spacing input into a {l, r, t, b} object.
**spacing**: A number, an array of length 2 or 4, or an object with l/r/t/b.
**Returns**: An object {l, r, t, b}.

10
docs/api/modules/video.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: "video"
type: docs
---
# video
### make_video() <sub>function</sub>
Decode a video file (MPEG, etc.) from an ArrayBuffer, returning a datastream object.

40
docs/api/prosperon.md Normal file
View File

@@ -0,0 +1,40 @@
---
title: "prosperon"
type: docs
---
# prosperon
### c_types <sub>object</sub>
### argv <sub>object</sub>
### version <sub>string</sub>
### revision <sub>string</sub>
### engine_start() <sub>function</sub>
### DOC <sub>symbol</sub>
### on(type, callback) <sub>function</sub>
### dispatch(type, data) <sub>function</sub>
### PATH <sub>object</sub>
### appupdate(...args) <sub>function</sub>
### update(...args) <sub>function</sub>
### physupdate(...args) <sub>function</sub>
### gui(...args) <sub>function</sub>
### hud(...args) <sub>function</sub>
### draw(...args) <sub>function</sub>
### imgui(...args) <sub>function</sub>
### app(...args) <sub>function</sub>

View File

@@ -0,0 +1,60 @@
---
title: "PHYSFS_File"
type: docs
---
# PHYSFS_File
A file handle opened via PhysFS for writing or reading. Freed automatically when references go away.
### close() <sub>function</sub>
Close this file handle. Throws on error.
**Returns**: None
### write(data) <sub>function</sub>
Write data (string or ArrayBuffer) to the file. Throws on error.
**data**: The data to write (string or ArrayBuffer).
**Returns**: None
### buffer(size) <sub>function</sub>
Enable an internal write buffer of the given size on this file.
**size**: Size in bytes of the buffer.
**Returns**: None
### tell() <sub>function</sub>
Return the current position in the file.
**Returns**: A numeric offset.
### eof() <sub>function</sub>
Return whether the file pointer is at end-of-file.
**Returns**: True if at EOF, false otherwise.

View File

@@ -0,0 +1,32 @@
---
title: "SDL_Camera"
type: docs
---
# SDL_Camera
A handle to a physical camera device. Freed when references drop or camera is closed.
### frame() <sub>function</sub>
Acquire the latest camera frame (as an SDL_Surface). Returns undefined if no
new frame is available yet. Throws on error.
**Returns**: SDL_Surface or undefined.
### release_frame(surface) <sub>function</sub>
Release a frame surface previously acquired via camera.frame(). Must be
done for each acquired frame.
**surface**: The surface to release.
**Returns**: None

View File

@@ -0,0 +1,9 @@
---
title: "SDL_Cursor"
type: docs
---
# SDL_Cursor
An SDL cursor handle. Freed automatically on GC. No direct methods.

View File

@@ -0,0 +1,8 @@
---
title: "SDL_GPUBuffer"
type: docs
---
# SDL_GPUBuffer
### name() <sub>function</sub>

View File

@@ -0,0 +1,236 @@
---
title: "SDL_GPUCommandBuffer"
type: docs
---
# SDL_GPUCommandBuffer
A command buffer that accumulates rendering, copy, and compute operations. Freed after submission or GC.
### render_pass(passDesc) <sub>function</sub>
Begin a render pass with color/depth attachments. Provide an object with
'color_targets' and optional 'depth_stencil'. Returns an SDL_GPURenderPass handle.
**passDesc**: {color_targets:[...], depth_stencil:...}
**Returns**: SDL_GPURenderPass
### compute_pass(storageTextures, storageBuffers) <sub>function</sub>
Begin a compute pass reading/writing given arrays of textures and buffers.
**storageTextures**: array of read/write textures
**storageBuffers**: array of read/write buffers
**Returns**: SDL_GPUComputePass
### swapchain_pass(clearColor) <sub>function</sub>
Begin a render pass that directly targets the swapchain (the window). Clears
with the specified color.
**clearColor**: [r,g,b,a]
**Returns**: SDL_GPURenderPass
### acquire_swapchain() <sub>function</sub>
Acquire the current swapchain texture from the window. Internal usage.
**Returns**: SDL_GPUTexture handle
### bind_vertex_buffer(slot, buffer) <sub>function</sub>
Bind a GPU buffer as the vertex buffer at a given slot.
**slot**: Integer slot index.
**buffer**: The SDL_GPUBuffer.
**Returns**: None
### bind_index_buffer(buffer, offset) <sub>function</sub>
Bind a GPU buffer as the index buffer (16-bit or 32-bit).
**buffer**: The SDL_GPUBuffer.
**offset**: Optional offset in bytes.
**Returns**: None
### bind_fragment_sampler(slot, texture, sampler) <sub>function</sub>
Bind a texture+sampler pair to a particular fragment shader slot.
**slot**: Index of the sampler binding.
**texture**: The SDL_GPUTexture
**sampler**: The SDL_GPUSampler
**Returns**: None
### push_vertex_uniform_data(slot, data) <sub>function</sub>
Push raw data to a vertex shader uniform block.
**slot**: The uniform buffer slot.
**data**: An ArrayBuffer with the data to upload.
**Returns**: None
### push_fragment_uniform_data(slot, data) <sub>function</sub>
Push raw data to a fragment shader uniform block.
**slot**: The uniform buffer slot index.
**data**: An ArrayBuffer with uniform data.
**Returns**: None
### push_compute_uniform_data(slot, data) <sub>function</sub>
Push raw data to a compute shader uniform buffer.
**slot**: The uniform buffer slot.
**data**: An ArrayBuffer with the data.
**Returns**: None
### submit() <sub>function</sub>
Submit this command buffer to the GPU and return a fence for synchronization.
**Returns**: An SDL_GPUFence
### cancel() <sub>function</sub>
Cancel (discard) this command buffer without submitting.
**Returns**: None
### camera(cameraTransform, uniformSlot) <sub>function</sub>
Write a camera transform (projection/view) to a uniform slot for 3D or 2D usage.
**cameraTransform**: A camera object or transform with .pos, fov, etc.
**uniformSlot**: The integer uniform buffer slot to which data is pushed.
**Returns**: None
### hud(sizeVec2, uniformSlot) <sub>function</sub>
Write an orthographic full-screen "HUD" matrix to a uniform slot. Typically used
for 2D overlays.
**sizeVec2**: [width, height] of the viewport area.
**uniformSlot**: The integer uniform buffer slot.
**Returns**: None
### push_debug_group(name) <sub>function</sub>
Push a named debug group marker onto the GPU command list (for debuggers/profilers).
**name**: The debug label string.
**Returns**: None
### pop_debug_group() <sub>function</sub>
Pop the most recent debug group marker.
**Returns**: None
### debug_label(label) <sub>function</sub>
Insert a one-off debug label at the current spot in the command list.
**label**: The debug label string
**Returns**: None
### blit(blitDesc) <sub>function</sub>
Blit one GPU texture to another with optional flip mode, filter, and clear operations.
**blitDesc**: { src:{texture,mip_level, etc}, dst:{texture,...}, load_op, flip, filter, clear_color:[r,g,b,a] }
**Returns**: None

View File

@@ -0,0 +1,88 @@
---
title: "SDL_GPUComputePass"
type: docs
---
# SDL_GPUComputePass
A compute pass for dispatching compute pipelines. Freed after end() or GC.
### dispatch(x, y, z) <sub>function</sub>
Dispatch the compute pipeline with the specified threadgroup counts.
**x**: Number of groups in X dimension
**y**: Number of groups in Y dimension
**z**: Number of groups in Z dimension
**Returns**: None
### end() <sub>function</sub>
End this compute pass.
**Returns**: None
### pipeline(computePipeline) <sub>function</sub>
Bind a compute pipeline in this pass.
**computePipeline**: The SDL_GPUComputePipeline
**Returns**: None
### samplers(arrayOfSamplerBindings, firstSlot) <sub>function</sub>
Bind a set of texture/sampler pairs for compute usage.
**arrayOfSamplerBindings**: e.g. [ {texture, sampler}, ...]
**firstSlot**: The starting sampler slot.
**Returns**: None
### storage_buffers(arrayOfBuffers, firstSlot) <sub>function</sub>
Bind an array of storage buffers for the compute shader.
**arrayOfBuffers**: The buffers
**firstSlot**: Starting binding slot.
**Returns**: None
### storage_textures(arrayOfTextures, firstSlot) <sub>function</sub>
Bind an array of storage textures for the compute shader.
**arrayOfTextures**: The textures
**firstSlot**: Starting binding slot
**Returns**: None

View File

@@ -0,0 +1,10 @@
---
title: "SDL_GPUComputePipeline"
type: docs
---
# SDL_GPUComputePipeline
Encapsulates a compute shader program plus associated resource layouts.
Created via device.compute_pipeline(...).

View File

@@ -0,0 +1,9 @@
---
title: "SDL_GPUCopyPass"
type: docs
---
# SDL_GPUCopyPass
A pass for CPU<->GPU or GPU<->GPU copy operations. No direct JS API besides internal usage.

View File

@@ -0,0 +1,241 @@
---
title: "SDL_GPUDevice"
type: docs
---
# SDL_GPUDevice
A handle for low-level GPU operations via SDL GPU. Freed on GC.
### claim_window(window) <sub>function</sub>
Claim an existing SDL_Window so this GPU device can render to it.
**window**: The SDL_Window to attach.
**Returns**: None
### make_pipeline(pipelineDesc) <sub>function</sub>
Create a new graphics pipeline from a descriptor object specifying shaders,
blend states, vertex format, etc.
**pipelineDesc**: An object containing pipeline fields (vertexShader, blend, etc.).
**Returns**: A SDL_GPUGraphicsPipeline handle.
### compute_pipeline(desc) <sub>function</sub>
Create a compute pipeline from a descriptor (shader code, threadgroup sizes, etc.).
**desc**: An object with shader code, thread counts, etc.
**Returns**: SDL_GPUComputePipeline handle.
### set_swapchain(composition, presentMode) <sub>function</sub>
Specify how the swapchain (final rendered image) is composed, e.g. 'sdr', 'hdr',
and present mode like 'vsync' or 'immediate'.
**composition**: E.g. 'sdr', 'linear', or 'hdr'.
**presentMode**: E.g. 'vsync', 'immediate', 'mailbox'.
**Returns**: None
### sort_sprite(a, b) <sub>function</sub>
A comparator function used for sorting sprite objects by layer, y, and texture.
Usually used internally.
**a**: A sprite object.
**b**: Another sprite object.
**Returns**: <0, 0, or >0 for sort ordering.
### make_sampler(samplerDesc) <sub>function</sub>
Create a sampler object specifying filtering, wrapping, anisotropy, etc.
**samplerDesc**: An object with min_filter, mag_filter, etc.
**Returns**: SDL_GPUSampler handle.
### load_texture(surface, compressionLevel) <sub>function</sub>
Upload an SDL_Surface into a GPU texture, optionally compressing with DXT. Freed automatically.
**surface**: An SDL_Surface.
**compressionLevel**: 0=none, 1=DXT1 or DXT5, 2=high quality, etc.
**Returns**: SDL_GPUTexture
### texture(desc) <sub>function</sub>
Create a GPU texture with the specified format usage.
**desc**: Object with {width, height, layers, type, format, usage, etc.}
**Returns**: SDL_GPUTexture
### make_quad() <sub>function</sub>
Return a simple 2-triangle quad geometry covering [0,1]x[0,1].
Useful for post-processing passes.
**Returns**: A mesh {pos, uv, color, indices}.
### driver() <sub>function</sub>
Return the name of the underlying GPU driver in use (e.g. 'OpenGL').
**Returns**: A string with driver name.
### make_shader(desc) <sub>function</sub>
Compile raw shader code (vertex or fragment) in e.g. SPIR-V, MSL, or DXIL format.
**desc**: {code:ArrayBuffer, stage:'vertex'|'fragment', format:'spv'|..., entrypoint:'main', ...}
**Returns**: SDL_GPUShader object
### acquire_cmd_buffer() <sub>function</sub>
Obtain a new command buffer for recording GPU commands. Must be submitted or canceled.
**Returns**: SDL_GPUCommandBuffer handle
### upload(cmdBuffer, buffers, transferBuffer) <sub>function</sub>
Upload CPU data into a list of GPU buffers, optionally reusing or returning a
transfer buffer. Typically you provide (cmdBuf, arrayOfTypedArrays, [transferBuffer]).
**cmdBuffer**: The command buffer in which to record copy commands.
**buffers**: An array of typed-array data to upload, each must have a 'gpu' property or so.
**transferBuffer**: Optional existing GPU transfer buffer to reuse.
**Returns**: The transfer buffer used or newly created.
### wait_for_fences(fences, waitAll) <sub>function</sub>
Wait on an array of GPU fence objects, optionally requiring all or any.
**fences**: An array of SDL_GPUFence objects.
**waitAll**: Boolean, true to wait for all fences, false for any.
**Returns**: True if fences signaled, false on timeout or error.
### query_fence(fence) <sub>function</sub>
Check if the given fence has been signaled yet. Non-blocking.
**fence**: SDL_GPUFence handle
**Returns**: True if signaled, false if still pending
### shader_format() <sub>function</sub>
Return an array of supported GPU shader binary formats (like 'spv', 'dxbc', etc.).
**Returns**: Array of strings naming supported formats.
### slice9(texture, dstRect, edges) <sub>function</sub>
Generate a 9-slice tiling geometry in one shot. For advanced usage with GPU pipeline.
**texture**: An SDL_GPUTexture
**dstRect**: The rectangle {x, y, w, h}
**edges**: {l, r, t, b} edge sizes
**Returns**: A mesh object
### tile(texture, srcRect, dstRect, tileInfo) <sub>function</sub>
Generate geometry to tile a texture portion inside a dest rect.
Often used for repeating backgrounds.
**texture**: The SDL_GPUTexture
**srcRect**: The portion to tile in pixels
**dstRect**: Where to fill
**tileInfo**: e.g. {repeat_x:true, repeat_y:true}
**Returns**: A mesh object

View File

@@ -0,0 +1,10 @@
---
title: "SDL_GPUFence"
type: docs
---
# SDL_GPUFence
A GPU fence for synchronization. Created upon commandBuffer.submit().
Wait or query it with device.wait_for_fences or device.query_fence.

View File

@@ -0,0 +1,10 @@
---
title: "SDL_GPUGraphicsPipeline"
type: docs
---
# SDL_GPUGraphicsPipeline
Encapsulates vertex+fragment shaders, blend/cull states, and vertex attribute layouts.
Created via device.make_pipeline(...).

View File

@@ -0,0 +1,164 @@
---
title: "SDL_GPURenderPass"
type: docs
---
# SDL_GPURenderPass
A single pass of drawing commands with color/depth attachments. Freed after end() or GC.
### bind_pipeline(pipeline) <sub>function</sub>
Bind a previously created graphics pipeline (shaders, states, vertex layouts, etc.).
**pipeline**: The SDL_GPUGraphicsPipeline
**Returns**: None
### viewport(rect) <sub>function</sub>
Set the viewport for clipping or scaling draws, in pass-local coordinates.
**rect**: {x,y,w,h}
**Returns**: None
### scissor(rect) <sub>function</sub>
Set a scissor rectangle for discarding pixels outside it.
**rect**: {x,y,w,h}
**Returns**: None
### draw(primitiveType, baseVertex, firstVertex, vertexCount) <sub>function</sub>
Issue a non-indexed draw call.
**primitiveType**: e.g. SDL_GPU_PRIMITIVETYPE_TRIANGLELIST
**baseVertex**: Starting vertex offset.
**firstVertex**: The first vertex to draw.
**vertexCount**: How many vertices to draw.
**Returns**: None
### draw_indexed(primitiveType, baseVertex, firstIndex, indexCount, instanceCount) <sub>function</sub>
Issue an indexed draw call from the bound index buffer.
**primitiveType**: The primitive type constant.
**baseVertex**: Offset in the vertex buffer.
**firstIndex**: Which index to start from.
**indexCount**: Number of indices to draw.
**instanceCount**: For instanced drawing, or 1 if normal.
**Returns**: None
### end() <sub>function</sub>
End this render pass, finalizing the draw operations.
**Returns**: None
### bind_index_buffer(buffer, elementSize16bit) <sub>function</sub>
Bind an index buffer inside this pass, possibly overriding the global one.
**buffer**: The SDL_GPUBuffer
**elementSize16bit**: If 2, uses 16-bit indices; if 4, uses 32-bit indices
**Returns**: None
### bind_buffers(firstSlot, arrayOfBuffers) <sub>function</sub>
Bind multiple vertex buffers at consecutive slots.
**firstSlot**: The starting vertex buffer slot.
**arrayOfBuffers**: An array of GPUBuffer objects
**Returns**: None
### bind_samplers(vertexOrFragment, firstSlot, samplerBindings) <sub>function</sub>
Bind multiple texture/sampler pairs to either vertex or fragment slots.
**vertexOrFragment**: Boolean, true for vertex stage, false for fragment.
**firstSlot**: The first sampler slot to bind.
**samplerBindings**: An array of {texture, sampler}.
**Returns**: None
### bind_storage_buffers(firstSlot, buffers) <sub>function</sub>
Bind one or more storage buffers for read/write in the pipeline.
**firstSlot**: Starting buffer slot index.
**buffers**: An array of SDL_GPUBuffer objects.
**Returns**: None
### bind_storage_textures(firstSlot, textures) <sub>function</sub>
Bind one or more storage textures for read/write in the pipeline.
**firstSlot**: Starting texture slot index.
**textures**: An array of SDL_GPUTexture objects.
**Returns**: None

View File

@@ -0,0 +1,9 @@
---
title: "SDL_GPUSampler"
type: docs
---
# SDL_GPUSampler
Defines how a texture is sampled (filter mode, address mode, anisotropy, compare op, etc.).

View File

@@ -0,0 +1,10 @@
---
title: "SDL_GPUShader"
type: docs
---
# SDL_GPUShader
A single compiled shader (vertex or fragment) in a GPU-friendly format
(e.g., SPIR-V, MSL). Combined into a pipeline for drawing.

View File

@@ -0,0 +1,8 @@
---
title: "SDL_GPUTexture"
type: docs
---
# SDL_GPUTexture
### name() <sub>function</sub>

View File

@@ -0,0 +1,10 @@
---
title: "SDL_GPUTransferBuffer"
type: docs
---
# SDL_GPUTransferBuffer
A staging buffer used for copying data to or from GPU buffers/textures. Typically
allocated/used internally by device.upload(...).

View File

@@ -0,0 +1,338 @@
---
title: "SDL_Renderer"
type: docs
---
# SDL_Renderer
A 2D rendering context using the SDL renderer API. Freed automatically.
### draw_color(color) <sub>function</sub>
Set the render draw color for subsequent primitive calls (rect, line, etc.).
**color**: [r, g, b, a] in 0..1.
**Returns**: None
### present() <sub>function</sub>
Display whatever has been rendered (swap buffers). Must be called each frame.
**Returns**: None
### clear() <sub>function</sub>
Clear the current render target with the renderer's draw color.
**Returns**: None
### rect(rectOrArray, color) <sub>function</sub>
Draw one or more outlines of rectangles.
**rectOrArray**: A single rect {x,y,w,h} or an array of rects.
**color**: Optional [r,g,b,a]. If provided, overrides current draw color.
**Returns**: None
### fillrect(rectOrArray, color) <sub>function</sub>
Fill one or more rectangles with the renderer's current color or an optional override.
**rectOrArray**: A single rect {x,y,w,h} or an array of rects.
**color**: Optional [r,g,b,a].
**Returns**: None
### line(points, color) <sub>function</sub>
Draw a sequence of lines connecting points in an array.
**points**: An array of [x,y] points. Lines connect consecutive points.
**color**: Optional [r,g,b,a].
**Returns**: None
### point(points, color) <sub>function</sub>
Draw a list of points (pixels).
**points**: An array of [x,y] positions.
**color**: Optional [r,g,b,a].
**Returns**: None
### load_texture(surface) <sub>function</sub>
Create an SDL_Texture from a given SDL_Surface for use with this renderer.
**surface**: An SDL_Surface.
**Returns**: An SDL_Texture object.
### texture(tex, dstRect, srcRect, color) <sub>function</sub>
Draw a texture onto the render target.
**tex**: The SDL_Texture to draw.
**dstRect**: The destination rect {x, y, w, h}.
**srcRect**: Optional portion of the texture to draw {x, y, w, h}.
**color**: Optional color mod [r,g,b,a].
**Returns**: None
### slice9(tex, dstRect, edges, srcRect) <sub>function</sub>
Draw a texture with 9-slice scaling. The argument includes edges {l, r, t, b}
for the corners/borders that remain unscaled. The rest is tiled or stretched.
**tex**: The SDL_Texture.
**dstRect**: Destination region {x, y, w, h}.
**edges**: {l, r, t, b} for corner sizes in pixels.
**srcRect**: Optional portion in the texture.
**Returns**: None
### tile(tex, dstRect, srcRect, scale) <sub>function</sub>
Tile a texture repeatedly within the specified region. Optionally use a srcRect.
**tex**: The SDL_Texture to tile.
**dstRect**: The region to fill {x, y, w, h}.
**srcRect**: Optional portion of texture.
**scale**: A float scale factor for each tile.
**Returns**: None
### get_image(rect) <sub>function</sub>
Read back the rendered pixels into a new SDL_Surface. If rect is undefined, capture entire output.
**rect**: Optional {x,y,w,h}.
**Returns**: An SDL_Surface with the requested region's pixels.
### fasttext(text, pos, color) <sub>function</sub>
Draw debug text using an internal fast path. Typically used for quick debugging overlays.
**text**: The string to draw.
**pos**: The [x, y] position to draw text.
**color**: Optional [r,g,b,a].
**Returns**: None
### geometry(texture, meshObject) <sub>function</sub>
Render custom geometry from a mesh object {pos, uv, color, indices, count} with an optional texture.
**texture**: The SDL_Texture or undefined.
**meshObject**: The geometry data with typed arrays.
**Returns**: None
### scale(scaleVec2) <sub>function</sub>
Set a scaling factor for all subsequent rendering on this renderer.
**scaleVec2**: [sx, sy] scaling factors.
**Returns**: None
### logical_size(size) <sub>function</sub>
Set a "logical" size that the renderer will scale to.
For example, (320, 240) can auto-scale up to the window resolution.
**size**: [width, height].
**Returns**: None
### viewport(rect) <sub>function</sub>
Set the clipping viewport for rendering. Pass undefined to use the full render target.
**rect**: {x, y, w, h}, or undefined.
**Returns**: None
### clip(rect) <sub>function</sub>
Set or clear the clipping rectangle for drawing. Pass undefined to clear.
**rect**: {x, y, w, h} or undefined.
**Returns**: None
### vsync(flag) <sub>function</sub>
Enable or disable vertical sync. This may have no effect depending on the driver.
**flag**: True or false.
**Returns**: None
### coords(pos) <sub>function</sub>
Convert window coordinates to this renderer's coordinate space.
**pos**: [x, y] in window space.
**Returns**: [x, y] in renderer coordinate space.
### camera(cameraTransform, centered) <sub>function</sub>
Set up a basic 2D camera matrix from a given transform. If 'centered' is true,
the origin is the center of the viewport, else top-left.
**cameraTransform**: The transform whose pos is used.
**centered**: Boolean true or false.
**Returns**: None
### get_viewport() <sub>function</sub>
Return the current viewport rect.
**Returns**: {x, y, w, h}
### screen2world(pos) <sub>function</sub>
Convert a screen coordinate to world space based on the current camera transform.
**pos**: [x, y] screen coords
**Returns**: [wx, wy] in world space
### target(texture) <sub>function</sub>
Set or clear the current render target texture. Pass undefined to reset to the default/window.
**texture**: An SDL_Texture or undefined
**Returns**: None
### make_sprite_mesh(sprites) <sub>function</sub>
Generate a mesh from an array of sprite objects, combining their positions, UVs,
and colors into a single geometry block.
**sprites**: An array of sprite-like objects.
**Returns**: A 'mesh' object with pos, uv, color, indices, etc.

View File

@@ -0,0 +1,73 @@
---
title: "SDL_Surface"
type: docs
---
# SDL_Surface
A software (CPU) image in memory. Freed when references vanish. Typically converted
to SDL_Texture for drawing, or used as raw pixel data.
### blit(dstRect, srcSurface, srcRect) <sub>function</sub>
Blit (copy) another surface onto this surface, scaling if needed.
**dstRect**: Destination {x, y, w, h}
**srcSurface**: The source SDL_Surface
**srcRect**: {x, y, w, h} portion from source
**Returns**: None
### scale(newSize) <sub>function</sub>
Return a new SDL_Surface scaled to [width, height] using linear filtering.
**newSize**: [width, height]
**Returns**: A new SDL_Surface with the scaled result.
### fill(color) <sub>function</sub>
Fill the entire surface with a single color.
**color**: [r, g, b, a] in 0..1
**Returns**: None
### rect(rect, color) <sub>function</sub>
Fill a sub-rectangle of the surface with a color.
**rect**: {x, y, w, h}
**color**: [r, g, b, a]
**Returns**: None
### dup() <sub>function</sub>
Make a copy of this surface in RGBA format.
**Returns**: A new SDL_Surface copy.

View File

@@ -0,0 +1,21 @@
---
title: "SDL_Texture"
type: docs
---
# SDL_Texture
A 2D GPU-accelerated texture for rendering with SDL_Renderer. Freed automatically.
### mode(mode) <sub>function</sub>
Set texture scale mode or filtering mode (nearest/linear).
**mode**: A string or numeric mode to set (e.g., 'linear').
**Returns**: None

View File

@@ -0,0 +1,20 @@
---
title: "SDL_Thread"
type: docs
---
# SDL_Thread
A handle to an SDL-created thread. Freed on GC after join.
Note: The engine generally doesn't expose custom usage for threads.
### wait() <sub>function</sub>
Block until this thread terminates.
**Returns**: None

View File

@@ -0,0 +1,131 @@
---
title: "SDL_Window"
type: docs
---
# SDL_Window
An application window, created via prosperon.engine_start or SDL calls. Freed on GC.
### fullscreen() <sub>function</sub>
Toggle fullscreen mode for this window (SDL_WINDOW_FULLSCREEN).
**Returns**: None
### make_renderer(name) <sub>function</sub>
Create an SDL_Renderer for 2D rendering tied to this window.
**name**: The renderer driver name, e.g. "opengl" (may be optional).
**Returns**: An SDL_Renderer object.
### make_gpu(debug, driverName) <sub>function</sub>
Create an SDL_GPUDevice for low-level GPU rendering on this window.
**debug**: If true, enable debugging in the GPU device.
**driverName**: The GPU back-end driver, e.g. "opengl".
**Returns**: An SDL_GPUDevice.
### keyboard_shown() <sub>function</sub>
Return whether the on-screen keyboard is visible (mobile/tablet).
**Returns**: True if shown, false otherwise.
### theme() <sub>function</sub>
Currently returns undefined. Placeholder for retrieving OS window theme info.
**Returns**: undefined
### safe_area() <sub>function</sub>
Return a rect describing any OS-specific "safe" region for UI, e.g. on iPhone with a notch.
**Returns**: A rect object {x, y, w, h}.
### bordered(flag) <sub>function</sub>
Enable or disable window borders.
**flag**: True to show borders, false to hide.
**Returns**: None
### set_icon(surface) <sub>function</sub>
Set the window's icon from an SDL_Surface.
**surface**: An SDL_Surface holding the icon.
**Returns**: None
### title <sub>accessor</sub>
Get or set the window's title text in the title bar.
**newTitle**: (when setting) A string title.
**Returns**: The current title if getting, or None if setting.
### size <sub>accessor</sub>
Get or set the window's size as [width, height].
**newSize**: (when setting) e.g. [640, 480]
**Returns**: The current [width, height] or None if setting.
### mouse_grab(flag) <sub>function</sub>
Grab or ungrab the mouse for this window (so the pointer won't leave).
**flag**: True to grab mouse input, false to release.
**Returns**: None

View File

@@ -0,0 +1,72 @@
---
title: "datastream"
type: docs
---
# datastream
A streaming media handle, typically for MPEG video. Freed automatically.
### time() <sub>function</sub>
Return the current playback time in seconds.
**Returns**: Current time as a float in seconds.
### seek(seconds) <sub>function</sub>
Seek to the specified time (in seconds).
**seconds**: The time to jump to in the stream.
**Returns**: None
### advance(seconds) <sub>function</sub>
Advance by a certain number of seconds, decoding video as needed.
**seconds**: The amount of time to skip forward.
**Returns**: None
### duration() <sub>function</sub>
Return the total duration of the video stream, in seconds, if known.
**Returns**: Float seconds duration, or 0 if unknown.
### framerate() <sub>function</sub>
Return the framerate (FPS) of the stream if known.
**Returns**: Float frames per second, or 0 if unknown.
### callback <sub>accessor</sub>
A function to call whenever a new frame is decoded. If not set, no callback is invoked.
**fn**: (when setting) A function that receives (surface).
**Returns**: The existing function or undefined if none.

View File

@@ -0,0 +1,71 @@
---
title: "enet_host"
type: docs
---
# enet_host
### service(callback, timeout) <sub>function</sub>
Poll for and process any available network events (connect, receive, disconnect, or none)
from this host, calling the provided callback for each event. This function loops until
no more events are available in the current timeframe.
Event object properties:
- type: String, one of "connect", "receive", "disconnect", or "none".
- peer: (present if type = "connect") The ENetPeer object for the new connection.
- channelID: (present if type = "receive") The channel on which the data was received.
- data: (present if type = "receive") The received data as a *plain JavaScript object*.
If the JSON parse fails or the data isn't an object, a JavaScript error is thrown.
object as its single argument.
**callback**: A function called once for each available event, receiving an event
**timeout**: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
**Returns**: None
### connect(host, port) <sub>function</sub>
Initiate a connection from this host to a remote server. Throws an error if the
connection cannot be started.
**host**: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
**port**: The port number to connect to.
**Returns**: An ENetPeer object representing the connection.
### flush() <sub>function</sub>
Flush all pending outgoing packets for this host immediately.
**Returns**: None
### broadcast(data) <sub>function</sub>
Broadcast a JavaScript object to all connected peers on channel 0. The object is
serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails.
**data**: A JavaScript object to broadcast to all peers.
**Returns**: None

107
docs/api/types/enet_peer.md Normal file
View File

@@ -0,0 +1,107 @@
---
title: "enet_peer"
type: docs
---
# enet_peer
### send(data) <sub>function</sub>
Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and
sent reliably. Throws an error if serialization fails.
**data**: A JavaScript object to send.
**Returns**: None
### disconnect() <sub>function</sub>
Request a graceful disconnection from this peer. The connection will close after
pending data is sent.
**Returns**: None
### disconnect_now() <sub>function</sub>
Immediately terminate the connection to this peer, discarding any pending data.
**Returns**: None
### disconnect_later() <sub>function</sub>
Request a disconnection from this peer after all queued packets are sent.
**Returns**: None
### reset() <sub>function</sub>
Reset this peer's connection, immediately dropping it and clearing its internal state.
**Returns**: None
### ping() <sub>function</sub>
Send a ping request to this peer to measure latency.
**Returns**: None
### throttle_configure(interval, acceleration, deceleration) <sub>function</sub>
Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
rate based on packet loss or congestion.
**interval**: The interval (ms) between throttle adjustments.
**acceleration**: The factor to increase sending speed when conditions improve.
**deceleration**: The factor to decrease sending speed when conditions worsen.
**Returns**: None
### timeout(timeout_limit, timeout_min, timeout_max) <sub>function</sub>
Set timeout parameters for this peer, determining how long ENet waits before considering
the connection lost.
**timeout_limit**: The total time (ms) before the peer is disconnected.
**timeout_min**: The minimum timeout (ms) used for each timeout attempt.
**timeout_max**: The maximum timeout (ms) used for each timeout attempt.
**Returns**: None

71
docs/api/types/font.md Normal file
View File

@@ -0,0 +1,71 @@
---
title: "font"
type: docs
---
# font
A bitmap or TTF-based font object storing glyph data.
Used for measuring/drawing text. Freed when GC sees no references.
### linegap <sub>accessor</sub>
Get or set the font's additional line spacing above the built-in metrics.
**value**: (when setting) The new line gap.
**Returns**: The current line gap (when getting), or None (when setting).
### height <sub>accessor</sub>
(read only)
The baseline-to-baseline height in pixels.
**Returns**: The font's total height in px.
### ascent <sub>accessor</sub>
(read only)
How far above the baseline the font extends.
**Returns**: A scalar float for ascent.
### descent <sub>accessor</sub>
(read only)
How far below baseline the font extends.
**Returns**: A scalar float for descent.
### text_size(text, letterSpacing, wrap) <sub>function</sub>
Measure a piece of text's width/height when rendered with this font.
**text**: The string to measure.
**letterSpacing**: Extra spacing between characters.
**wrap**: If nonzero, word-wrap to this maximum width.
**Returns**: [width, height] as a float array.

89
docs/api/types/rtree.md Normal file
View File

@@ -0,0 +1,89 @@
---
title: "rtree"
type: docs
---
# rtree
An R-tree for spatial lookups. Insert bounding boxes, query by bounding box, etc.
### add(obj) <sub>function</sub>
Insert an object that has a 'rect' property {x, y, w, h} into the tree.
**obj**: The object to add (must have rectAtom).
**Returns**: None
### delete(obj) <sub>function</sub>
Remove an object from the tree. Must match the same rect as used when adding.
**obj**: The object to remove.
**Returns**: None
### query(rect) <sub>function</sub>
Return an array of objects whose bounding boxes intersect the given rect.
**rect**: {x, y, w, h} bounding region to query.
**Returns**: Array of objects that overlap that region.
### size <sub>accessor</sub>
(read only)
Indicates how many items are stored in the rtree.
**Returns**: Integer count of items in the tree.
### forEach(callback) <sub>function</sub>
Call a function for every item in the rtree.
**callback**: A function called with no arguments, or possibly (item).
**Returns**: None
### has(obj) <sub>function</sub>
Return true if the specified object is in the tree, false otherwise.
**obj**: The object to check.
**Returns**: True if found, else false.
### values() <sub>function</sub>
Return an array of all items currently in the rtree.
**Returns**: Array of all stored objects.

73
docs/api/types/sprite.md Normal file
View File

@@ -0,0 +1,73 @@
---
title: "sprite"
type: docs
---
# sprite
A 'sprite' is a simple struct for 2D drawing. It stores a rectangle (pos + size),
UV coordinates, color, and layer, as well as an associated 'image' object. The sprite
can be drawn via GPU or SDL_Renderer. Freed when no JS references remain.
### set_affine(transform) <sub>function</sub>
Update this sprite's position and size from a transform's pos and scale.
**transform**: The transform whose pos/scale will overwrite the sprite's rect.
**Returns**: None
### set_rect(rect) <sub>function</sub>
Set the sprite's rect (x, y, w, h) directly.
**rect**: An object or array specifying x, y, width, and height.
**Returns**: None
### set_image(image) <sub>function</sub>
Assign or replace the sprite's underlying image. Automatically updates UV if
the image has a 'rect' property.
**image**: A JS object representing the image (with .texture, .rect, etc.).
**Returns**: None
### layer <sub>accessor</sub>
Get or set the sprite's z-layer integer. Sprites with higher layers typically
draw on top of lower layers.
**value**: (when setting) An integer specifying the layer.
**Returns**: The current layer (when getting), or None (when setting).
### color <sub>accessor</sub>
Get or set the sprite's color tint as [r, g, b, a].
**value**: (when setting) An array [r, g, b, a] in the 0.0..1.0 range.
**Returns**: The current color array (when getting), or None (when setting).

34
docs/api/types/timer.md Normal file
View File

@@ -0,0 +1,34 @@
---
title: "timer"
type: docs
---
# timer
A scheduled callback or countdown. Freed automatically once no longer referenced
or once it completes.
### remain <sub>accessor</sub>
Get or set how many seconds remain before the timer triggers.
**value**: (when setting) A float specifying new time remaining.
**Returns**: The current time left (when getting), or None (when setting).
### fn <sub>accessor</sub>
Get or set the function called when the timer expires.
**value**: (when setting) The function.
**Returns**: The function or undefined if none is set.

219
docs/api/types/transform.md Normal file
View File

@@ -0,0 +1,219 @@
---
title: "transform"
type: docs
---
# transform
A hierarchical transform storing 3D or 2D position, rotation (as a quaternion),
and scale. Can have a parent transform. Freed automatically on GC.
### pos <sub>accessor</sub>
Get or set the transform's position as a 3D vector [x, y, z].
**value**: (when setting) [x, y, z].
**Returns**: The current position vector (when getting), or None (when setting).
### scale <sub>accessor</sub>
Get or set the transform's scale as a 3D vector [x, y, z].
For 2D usage, z is often 1.
**value**: (when setting) [sx, sy, sz].
**Returns**: The current scale (when getting), or None (when setting).
### rotation <sub>accessor</sub>
Get or set the transform's rotation as a quaternion [x, y, z, w].
Angles in degrees or radians must first be converted prior to making a quaternion.
**value**: (when setting) [qx, qy, qz, qw].
**Returns**: The current quaternion (when getting), or None (when setting).
### parent <sub>accessor</sub>
Get or set the transform's parent. If set, this transform becomes a child of
the parent (re-parenting). Must be another transform object or undefined.
**value**: (when setting) Another transform or undefined.
**Returns**: The current parent transform (when getting), or None (when setting).
### change_hook <sub>accessor</sub>
A user-supplied function that's called whenever the transform's local matrix changes.
If undefined, no hook is called.
**value**: (when setting) A function.
**Returns**: The current function or undefined.
### trs(pos, quat, scale) <sub>function</sub>
Set the transform's position, rotation, and scale in one call.
**pos**: [x,y,z] for position, or undefined to keep existing.
**quat**: [qx,qy,qz,qw] for rotation, or undefined.
**scale**: [sx,sy,sz] for scale, or undefined.
**Returns**: None
### phys2d(velocity, angularVel, dt) <sub>function</sub>
Apply simple 2D velocity and angular velocity to this transform.
**velocity**: [vx, vy] added to 'pos' each frame.
**angularVel**: A scalar for rotation in (radians/second).
**dt**: The time delta in seconds.
**Returns**: None
### move(delta) <sub>function</sub>
Translate this transform by the specified vector.
**delta**: [dx, dy, dz] to add to .pos
**Returns**: None
### rotate(axis, angle) <sub>function</sub>
Rotate this transform by an axis+angle.
**axis**: [ax, ay, az] the axis of rotation.
**angle**: The angle in turns or radians (depending on usage).
**Returns**: None
### angle(axis) <sub>function</sub>
Return the transform's rotation about a specified axis (x, y, or z).
For example, angle([1,0,0]) returns the roll about the X-axis.
**axis**: Which axis [1,0,0] or [0,1,0] or [0,0,1].
**Returns**: The numeric angle in 'turns' or radians, depending on usage.
### lookat(target) <sub>function</sub>
Rotate this transform so it looks toward the given world position.
**target**: [x, y, z] position in world coords.
**Returns**: None
### direction(localDir) <sub>function</sub>
Rotate a vector by this transform's rotation, effectively "transforming"
a direction from local space to world space.
**localDir**: [dx, dy, dz] in local transform coordinates.
**Returns**: [dx', dy', dz'] direction in world space.
### unit() <sub>function</sub>
Reset position, rotation, and scale to [0,0,0], identity rotation, and [1,1,1].
**Returns**: None
### rect(rect) <sub>function</sub>
Set this transform's pos and scale from a 2D rect object {x, y, w, h}.
**rect**: Object with .x, .y, .w, .h
**Returns**: None
### array() <sub>function</sub>
Return this transform's matrix as a 16-element float array in column-major order.
**Returns**: An array of 16 floats.
### torect() <sub>function</sub>
Convert transform's 2D position/scale to a rect {x, y, w, h}.
Rotation is currently ignored.
**Returns**: A rect object {x, y, w, h}.
### children() <sub>function</sub>
Return an array of child transforms belonging to this transform.
**Returns**: An array of transform objects.

12
docs/api/use.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "use"
type: docs
---
# use
### length <sub>number</sub>
### name <sub>string</sub>
### prototype <sub>object</sub>

View File

@@ -0,0 +1,39 @@
# Actor Use Cases for Prosperon
Confirmed implementation targets. These are the concrete reasons actors add value beyond basic frame scheduling.
## 1. P2P Networking (Lockstep + Input Streaming)
Actor security model enables safe P2P:
- **Lockstep**: Both peers run the same actor with the same inputs. Actors are deterministic (same messages in = same state). Synchronize input streams.
- **Input streaming** (GGPO-style): Each peer sends inputs to the other. Actors only see inputs, not internal state, so cheaters can't fabricate state — only send inputs that the other peer validates by running the simulation.
- Security-first actor system makes this safe by design.
## 2. Game Sharing (DS Download Play Style)
Send entire cell packages over the network. Receiver runs the game in a sandboxed actor.
- A cell program/game sends its entire package (code + assets) using the cell package system.
- The other person runs it in a sandboxed actor — can't touch host filesystem, can't read save data, can only draw sprites and send messages through the allowed interface.
- Same trust model as mod sandboxing but for complete games.
## 3. Mod Sandboxing
Untrusted code runs in its own actor, communicates through a controlled message interface.
- Mods can't corrupt game state.
- Related to game sharing — same isolation model.
## 4. Background Asset Loading
Refactor the resource manager (currently single-threaded) into an actor:
- Game sends: `{type: 'load', kind: 'image', path: 'player'}`
- Resources replies: `{type: 'loaded', path: 'player', data: <blob>}`
- Game never blocks. Request assets, keep running, handle them when they arrive.
- Sprites show placeholder until real texture lands.
- This is the natural actor pattern: send a message, get a message back.
## 5. Editor <-> Game Isolation
Editor runs as a separate actor, sends commands to the game actor.
- Can't corrupt game state.
- Clean separation of concerns.
- Editor can inspect/modify game state only through the message interface.

166
docs/entities.md Normal file
View File

@@ -0,0 +1,166 @@
---
title: "Entities"
type: docs
---
# Entities and the World
Prosperon uses a **script + overrides** model for entities, similar to Source engine entities. An entity type is defined by a script (`.cm` module), and instances are records that override specific attributes.
## Entity Types
An entity type is a module that returns a prototype object:
```javascript
// entities/goblin.cm
var sprite = use('sprite')
return {
health: 100,
speed: 2,
image: "goblin.png",
init: function() {
this.sprite = sprite({
image: this.image,
pos: this.pos,
width: 32,
height: 32,
layer: 5
})
},
on_destroy: function() {
this.sprite.destroy()
}
}
```
The return value defines every valid field with a default. This prototype IS the schema — the editor and tooling can introspect it to know what fields exist and what their defaults are.
## Creating Entities
Use the `world` module to create entity instances:
```javascript
var world = use('world')
var goblin_proto = use('entities/goblin')
var goblin = world.add_entity(goblin_proto, {
pos: {x: 100, y: 200},
health: 50
})
```
The second argument overrides specific fields from the prototype. Unspecified fields keep their defaults.
## Entity Lifecycle
Entities have two lifecycle hooks:
- `init()` — called when the entity is created (after overrides are applied)
- `on_destroy()` — called when the entity is removed from the world
```javascript
world.destroy_entity(goblin)
```
## Levels as Data
A level is a collection of entity instances stored as JSON — each entry is a script path plus attribute overrides:
```json
[
{"script": "entities/goblin", "pos": {"x": 100, "y": 200}, "health": 50},
{"script": "entities/goblin", "pos": {"x": 300, "y": 200}},
{"script": "entities/tree", "pos": {"x": 200, "y": 150}},
{"script": "entities/player_spawn", "pos": {"x": 50, "y": 50}}
]
```
The world loads this file, creates each entity from its prototype with the overrides applied. Since levels are JSON, they're easy to diff in source control and simple to generate from an editor.
## Composition via Modules
Entity behavior comes from composition, not inheritance. A goblin that patrols and has health uses separate modules:
```javascript
// entities/patrol_goblin.cm
var health = use('systems/health')
var patrol = use('systems/patrol')
return {
health: 100,
patrol_speed: 1.5,
patrol_points: [],
init: function() {
health.attach(this)
patrol.attach(this, this.patrol_points)
}
}
```
Each system module provides behavior that operates on the entity's data. No class trees, no diamond inheritance — just modules that read and write fields.
## Querying Entities
The world module provides several ways to find and iterate entities:
```javascript
var world = use('world')
// Iterate all entities
world.each(function(entity) {
log.console(entity)
})
// Filter entities by predicate — returns array
var enemies = world.query(function(e) { return e.team == 'enemy' })
// Find first matching entity
var player = world.find(function(e) { return e.is_player })
// Get entity count
var n = world.count()
```
## Updating Entities
Call `world.update(dt)` each frame to tick all entities that have an `update(dt)` method:
```javascript
// In your core.start() update callback:
core.start({
update: function(dt) {
world.update(dt)
}
})
```
## Loading Levels
Load a level from a JSON array of entity definitions:
```javascript
world.load_level([
{"script": "entities/goblin", "pos": {"x": 100, "y": 200}},
{"script": "entities/tree", "pos": {"x": 300, "y": 100}}
])
```
Each entry's `script` field is loaded via `use()` as the prototype. All other fields are applied as overrides.
## Clearing the World
Remove and destroy all entities:
```javascript
world.clear()
```
## Override Rules
Overrides replace fields at the top level. If the prototype has `pos: {x: 0, y: 0}` and the override has `pos: {x: 50, y: 100}`, the entire `pos` is replaced. This is predictable and avoids ambiguity about partial nested merges.
Overrides should be pure data — field values only. No expressions, no conditionals, no logic. The script defines behavior; the override only tweaks data.

189
docs/graphics.md Normal file
View File

@@ -0,0 +1,189 @@
---
title: "Graphics"
type: docs
---
# Graphics Concepts
This page covers the fundamental graphics concepts in Prosperon.
## Textures and Images
A **texture** is a set of bytes on the GPU — not directly accessible from script code.
An **image** is a texture combined with a UV rect that specifies which region to draw:
```
image = {
texture: GPU texture handle,
rect: {x, y, width, height} // UV coordinates
}
```
This means multiple images can share one texture (a sprite sheet or atlas), each referencing a different region.
## Supported Image Formats
| Format | Notes |
|--------|-------|
| PNG | Standard, lossless |
| QOI | Fast decode, lossless |
| GIF | Animated frames supported |
| JPG/JPEG | Lossy |
| Aseprite (.ase) | Frames, tags, slices, durations, pivots |
Aseprite files are fully parsed — named animation tags, per-frame durations, pivot points, and slice data are all available.
## Sprites
A sprite is the fundamental drawable. Create one with the `sprite` module:
```javascript
var sprite = use('sprite')
var s = sprite({
image: "player.png",
pos: {x: 100, y: 200},
width: 32,
height: null, // derived from aspect ratio
anchor_x: 0.5, // center horizontally
anchor_y: 0, // bottom edge
layer: 5,
rotation: 0,
flip: {x: false, y: false},
color: {r: 1, g: 1, b: 1, a: 1},
opacity: 1,
tint: {r: 1, g: 1, b: 1, a: 1},
filter: 'nearest',
plane: 'default',
groups: [],
visible: true
})
```
### Fit Modes
When both `width` and `height` are specified, the `fit` property controls how the texture maps into the rectangle:
| Fit | Behavior |
|-----|----------|
| `none` | Use the texture's native pixel size |
| `fill` | Stretch to exactly fill the rectangle (may distort) |
| `contain` | Fit inside the rectangle, preserving aspect ratio |
| `cover` | Fill the rectangle, preserving aspect ratio (crops via UV) |
If only one dimension is given (e.g. `width: 32, height: null`), the other is derived from the texture's aspect ratio.
### Anchors
The anchor determines which point of the sprite sits at `pos`:
- `anchor_x: 0, anchor_y: 0` — top-left at pos
- `anchor_x: 0.5, anchor_y: 0.5` — center at pos
- `anchor_x: 0.5, anchor_y: 0` — bottom-center at pos (good for characters)
### UV Mapping
For sprite sheets, control which region of the texture is drawn:
```javascript
sprite({
image: spritesheet_texture,
uv: {offset: {x: 0.25, y: 0}, scale: {x: 0.25, y: 0.5}}
})
```
## Text
Create text with the `text2d` module:
```javascript
var text2d = use('text2d')
var label = text2d({
text: "Hello World",
font: "myfont.ttf",
size: 24,
pos: {x: 100, y: 100},
layer: 10
})
```
## Shapes
SDF-rendered shapes via the `shape2d` module:
```javascript
var shape2d = use('shape2d')
var box = shape2d.rect({
pos: {x: 50, y: 50},
width: 100,
height: 80,
color: {r: 1, g: 0, b: 0, a: 1},
layer: 3
})
var ball = shape2d.circle({
pos: {x: 200, y: 200},
radius: 25,
color: {r: 0, g: 1, b: 0, a: 1}
})
```
Available shapes: `rect`, `circle`, `ellipse`, `pill`.
## Tilemaps
Grid-based tile rendering:
```javascript
var tilemap2d = use('tilemap2d')
var map = tilemap2d({
tiles: tiles_2d_array,
tile_width: 16,
tile_height: 16,
layer: 0
})
```
Tiles can be flipped, and the entire tilemap can have an affine transform applied. Camera culling for large maps is planned.
## Particles
Particle emitters produce sprite-like particles:
```javascript
var particles2d = use('particles2d')
var emitter = particles2d({
texture: "spark.png",
pos: {x: 100, y: 100},
layer: 8
})
```
## Coordinate System
Prosperon uses a Y-up coordinate system:
- **X+** goes right
- **Y+** goes up
- `[0, 0]` in world space is where the camera starts
- `[0, 0]` in HUD space is the bottom-left of the screen
## Camera
A camera defines the viewport into the world. Its `width` and `height` set the game's internal resolution. Its `pos` determines what world coordinate is at the center of the screen.
```javascript
var camera = use('camera')
var cam = camera.make({
pos: {x: 0, y: 0},
width: 320,
height: 240
})
```
Everything in world space is drawn relative to the camera's position. HUD space is always screen-relative.

59
docs/index.md Normal file
View File

@@ -0,0 +1,59 @@
---
title: "Prosperon Manual"
type: docs
---
# Prosperon
Prosperon is a 2D game programming environment built on [Pit](https://crumbpit.org), an actor-based language designed for safe, sandboxed scripting.
You write game logic in Pit — a simplified JavaScript with actor primitives — and Prosperon handles rendering, input, audio, and asset management. Games are composed from small, focused modules. No class hierarchies, no giant scene trees.
## How It Works
A Prosperon game is a collection of **modules** (`.cm`) and **programs** (`.ce`).
- **Modules** return a frozen value — an API, a config, a data table. Import them with `use()`.
- **Programs** are actor entry points. They run top-to-bottom, register message handlers, and spawn children.
The engine provides modules for everything a 2D game needs:
| Module | Purpose |
|--------|---------|
| `sprite` | Create and register sprites |
| `draw2d` | High-level factories for sprites, text, shapes, tilemaps |
| `camera` | Create cameras with transform and projection |
| `input` | Multi-device action mapping with control stacks |
| `sound` | Audio playback with PCM caching and voice mixing |
| `tween` | Fire-and-forget animation with easing |
| `world` | Entity management and lifecycle |
| `resources` | Asset discovery and path resolution |
| `core` | Main loop, window, frame scheduling |
You create sprites, poke their positions, and the engine handles everything else — batching, sorting, effects, presentation scaling. Like programming sprites on a GBA, but with modern hardware behind it.
## Design Principles
**Minimal code.** The fewer lines it takes to do something, the fewer bugs it has, the faster it runs, and the less you need to keep in your head.
**Duck typing.** If an object looks like a sprite — has a position, an image, a layer — it works as a sprite. No base classes to inherit from.
**Text-based.** Every file in a Prosperon project is text. Scripts, configs, levels. Git diffs are readable, merges are clean.
**Modules, not globals.** Everything is accessed via `use()`. No global engine object. The world, camera, input — all imported as modules.
**Data-driven rendering.** Your game logic never touches the GPU. You declare drawables and their properties. The rendering pipeline sorts, batches, applies effects, and draws.
## Guides
- [Quickstart](quickstart/) — Run your first program
- [Tutorial](tutorial/) — Build a game step by step
- [Entities](entities/) — The world and entity model
- [Rendering](rendering/) — Sprites, cameras, and the rendering pipeline
- [Graphics](graphics/) — Textures, images, and coordinate systems
- [Input](input/) — Action mapping, devices, and control stacks
- [Resources](resources/) — Asset loading and discovery
## API Reference
- [API Index](api/) — All modules and types

148
docs/input.md Normal file
View File

@@ -0,0 +1,148 @@
---
title: "Input"
type: docs
---
# Input
Prosperon's input system normalizes keyboard, mouse, gamepad, and touch into **named actions**. You define what actions your game uses, bind them to physical inputs, and the engine routes events to the right place.
## Actions
An action is a named game event like `"jump"`, `"attack"`, or `"ui_up"`. Multiple physical inputs can trigger the same action:
```javascript
var input = use('input')
input.configure({
action_map: {
jump: ['space', 'gamepad_a'],
attack: ['mouse_button_left', 'gamepad_x'],
move_left: ['a', 'left', 'gamepad_dpleft'],
move_right: ['d', 'right', 'gamepad_dpright']
}
})
```
Default actions are provided for common UI navigation (`ui_up`, `ui_down`, `ui_left`, `ui_right`, `confirm`, `cancel`, `menu`).
## Players and Users
The input system supports multiple players. Each **user** has:
- Paired devices (keyboard, specific gamepad, etc.)
- An action map (bindings can differ per player)
- A **control stack** of possessed entities
```javascript
var p1 = input.player1()
```
### Device Pairing
By default, pairing is `'last_used'` — all input goes to player 1, and the active device switches when a button is pressed. For local multiplayer, use `'explicit'` pairing where each player is assigned specific devices.
```javascript
input.configure({
max_users: 2,
pairing: 'explicit'
})
```
## Control Stack (Possession)
Players **possess** entities to route input to them. The control stack is ordered — the topmost entity gets input first.
```javascript
var p1 = input.player1()
// Take control of the player character
p1.possess(player_entity)
// Push a menu on top — it gets input priority
p1.push(pause_menu)
// Pop the menu — player character gets input again
p1.pop()
```
The possessed entity must have an `on_input` method:
```javascript
var player = {
on_input: function(action, data) {
if (action == 'jump' && data.pressed) {
// jump logic
}
if (action == 'move_right') {
// data.pressed, data.released, data.time
}
}
}
```
## Device Detection
The input system tracks what kind of device is active, so you can show appropriate button prompts:
```javascript
var p1 = input.player1()
var kind = p1.device_kind() // 'keyboard', 'mouse', 'gamepad'
var type = p1.gamepad_type() // 'xbox', 'playstation', etc.
```
Get the display name or icon for an action based on the current device:
```javascript
var icon = p1.get_icon_for_action('jump')
var binding = p1.get_primary_binding('jump')
```
## Emacs-Style Keybindings
For editors or complex hotkey systems, Prosperon supports modifier notation:
| Prefix | Key |
|--------|-----|
| `C-` | Ctrl |
| `M-` | Alt |
| `S-` | Super |
So `C-a` means Ctrl+A, `C-M-s` means Ctrl+Alt+S. Case is preserved — `a` and `A` are different bindings.
The emacs module is enabled by default but can be disabled:
```javascript
input.configure({ emacs: false })
```
## Gestures
Touch gesture recognition is built in:
- Swipe detection (with configurable distance and time thresholds)
- Pinch detection
```javascript
input.configure({
gestures: true,
swipe_min_dist: 50,
swipe_max_time: 0.5
})
```
## Saved Bindings
Player bindings are automatically saved and loaded, so remapped controls persist across sessions.
## Raw Events
For debugging or special cases, the raw SDL event can be accessed via the config callback:
```javascript
core.start({
input: function(raw_event) {
// raw SDL event object
}
})
```

170
docs/ops.md Normal file
View File

@@ -0,0 +1,170 @@
---
title: "Compositor"
type: docs
---
# The Compositor
The compositor is Prosperon's rendering orchestrator. You describe your scene — planes, layers, effects — and the compositor figures out what render passes are needed and executes them.
## The Basic Idea
You don't issue draw calls. You create sprites and set their properties. The compositor does the rest:
1. Queries all registered drawables
2. Organizes them by plane and layer
3. Applies effects to tagged groups
4. Composites everything to the screen
Think of it like a GBA: you declare "sprite here, sprite there," get handles back, and poke their positions. The hardware (in this case, the compositor + GPU backend) handles the actual rendering.
## Scene Configuration
A scene is described as a config object passed to the compositor:
```javascript
var compositor = use('compositor')
var plan = compositor.compile({
clear: {r: 0, g: 0, b: 0, a: 1},
planes: [
{
name: 'background',
plane: 'background',
resolution: {width: 320, height: 240},
camera: bg_camera,
presentation: 'integer_scale'
},
{
name: 'game',
plane: 'default',
resolution: {width: 320, height: 240},
camera: game_camera,
layer_sort: {'5': 'y'},
presentation: 'integer_scale'
},
{
name: 'hud',
plane: 'hud',
resolution: {width: 1280, height: 720},
camera: hud_camera,
presentation: 'stretch'
}
]
})
```
Each plane renders independently at its own resolution and composites onto the screen.
## Planes
A **plane** is a named rendering group. Sprites belong to a plane via their `plane` property (default: `'default'`). Each plane in the compositor config:
- Has its own **resolution** (low-res pixel art, native UI, etc.)
- Has its own **camera**
- Has its own **layer sorting** rules
- Has a **presentation mode** (how it maps to the window)
Planes composite in order — later planes draw on top of earlier ones.
## Layer Sorting
Within a plane, drawables are sorted by `layer` (integer). Within each layer, you can choose a sorting mode:
```javascript
layer_sort: {
'0': 'explicit', // engine may reorder for batching efficiency
'5': 'y' // sort by Y position (top-down game depth)
}
```
Y-sorting is essential for top-down games where objects lower on screen should appear in front.
## Effects
Effects are applied to **groups** of sprites. Tag sprites with group names, then define effects for those groups:
```javascript
var player = sprite({
image: "player.png",
groups: ['glow_objects']
})
var plan = compositor.compile({
planes: [{
name: 'game',
plane: 'default',
camera: cam,
resolution: {width: 320, height: 240}
}],
group_effects: {
glow_objects: {
effects: [
{type: 'bloom', threshold: 0.5, intensity: 1.5, blur_passes: 2}
]
}
}
})
```
### Available Effects
**Bloom** — extracts bright areas, blurs them, composites back:
```javascript
{type: 'bloom', threshold: 0.8, intensity: 1.0, blur_passes: 2}
```
**Mask** — uses sprites in a mask group as a stencil:
```javascript
{type: 'mask', mask_group: 'mask_shapes', channel: 'alpha', invert: false}
```
Sprites in the `mask_group` are not drawn directly — they only serve as the mask shape.
## Presentation Modes
Each plane specifies how its render target maps to the window:
| Mode | Behavior |
|------|----------|
| `stretch` | Fill the window exactly (may distort aspect ratio) |
| `letterbox` | Fit inside window, preserving aspect ratio, with black bars |
| `integer_scale` | Scale by whole numbers only for pixel-perfect rendering |
`integer_scale` automatically uses nearest-neighbor filtering for crisp pixels.
## Execution
The compositor produces a **plan** — a list of render passes. The plan is executed by the GPU backend:
```javascript
var result = compositor.execute(plan)
// result.commands is sent to the GPU backend
```
In practice, `core.start({render})` handles this for you. Your render callback returns the compositor result and the engine executes it.
## Manual Drawables
You can inject drawables that aren't registered with film2d by adding them directly to a plane config:
```javascript
planes: [{
name: 'game',
plane: 'default',
camera: cam,
drawables: [
{type: 'sprite', image: tex, pos: {x: 0, y: 0}, width: 32, height: 32, layer: 0}
]
}]
```
These are merged with the registered drawables for that plane.
## Render Targets
The compositor automatically manages intermediate render targets for effects. You don't need to create or manage GPU textures — the compositor allocates and reuses them as needed.
## Debug
Pass `debug: 'graph'` to `core.start` config to log the compiled render plan as JSON. Pass `debug: 'cmd'` to log the final command list sent to the GPU.

BIN
docs/prosperon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

50
docs/prosperon.md Normal file
View File

@@ -0,0 +1,50 @@
---
title: "About Prosperon"
type: docs
---
# Prosperon
Prosperon is a 2D game programming environment built on [Pit](https://crumbpit.org), an actor-based language designed for safe, sandboxed scripting.
Games are written as small, focused modules in Pit — a simplified JavaScript with actor primitives. Prosperon handles rendering, input, audio, and asset management. No class hierarchies, no giant scene trees.
## Design Goals
- **Minimal API surface.** A handful of modules cover sprites, input, sound, and composition. Each does one thing.
- **Data-driven rendering.** You create sprites and set properties. The compositor handles batching, sorting, and GPU submission.
- **Composition over inheritance.** Entity behavior comes from modules, not class trees. An entity type is a script that returns a prototype object.
- **AI-friendly.** Many small files with simple dependencies. An LLM can read a module, understand it, and modify it.
- **Text-first.** Assets are referenced as string paths. Levels are JSON. Everything diffs cleanly in source control.
## Architecture
Prosperon is not a monolithic engine with a global state object. It is a collection of modules:
| Module | Purpose |
|--------|---------|
| `core` | Main loop, window, GPU init |
| `sprite` | Create and manage sprites |
| `compositor` | Build render plans from scene configs |
| `input` | Action mapping, device routing |
| `sound` | Audio playback |
| `world` | Entity management (add, query, update, levels) |
| `camera` | Viewport into the world |
| `draw2d` | Re-exports sprite, shape, text, tilemap, anim |
| `text2d` | Text rendering |
| `shape2d` | SDF shapes (rect, circle, ellipse, pill) |
| `anim2d` | Sprite animation playback (aseprite/gif) |
| `collision2d` | AABB + circle overlap queries |
| `tilemap2d` | Grid-based tile rendering |
| `tween` | Value interpolation |
| `resources` | Asset path resolution |
Import what you need with `use()`. If you don't use a module, it doesn't load.
## The Language
Pit is an actor-based language. Programs (`.ce` files) are actors that run top-to-bottom, can register message handlers, and spawn child actors. Modules (`.cm` files) return frozen values and are cached.
Actor primitives: `$start`, `$receiver`, `send`, `$delay`, `$clock`, `$stop`.
See [Quickstart](../quickstart/) for a hands-on introduction.

111
docs/quickstart.md Normal file
View File

@@ -0,0 +1,111 @@
---
title: "Quickstart"
type: docs
---
# Quickstart
This quickstart walks through creating modules, programs, and running examples.
## Your First Module
A module (`.cm`) returns a frozen value — an API, a config, data.
Create `hello.cm`:
```javascript
return {
greet: function(name) {
return `Hello, ${name}!`
}
}
```
## Your First Program
A program (`.ce`) is an entry point. It runs top-to-bottom and can register handlers.
Create `hello.ce`:
```javascript
var hello = use('hello')
log.console(hello.greet('Prosperon'))
$receiver(function(msg) {
if (msg.type == 'ping') {
send(msg, {type: 'pong'})
}
})
$delay(_ => $stop(), 0.1)
```
## Modules vs Programs
| | Modules (`.cm`) | Programs (`.ce`) |
|---|---|---|
| Must return a value | Yes | No |
| Return value frozen | Yes | N/A |
| Can register handlers | No | Yes (`$receiver`) |
| Can spawn children | No | Yes (`$start`) |
| Cached after first load | Yes | N/A |
## Spawning Actors
Programs can spawn other programs as child actors:
```javascript
$receiver(function(e) {
if (e.type == 'greet') {
log.console('Child greeted me')
}
})
$start(function(info) {
if (info.type == 'greet') {
log.console('Spawned child actor')
}
}, 'hello.ce')
$delay(_ => $stop(), 0.5)
```
## Actor Primitives
| Primitive | Purpose |
|-----------|---------|
| `$start(callback, path)` | Spawn a child actor from a program |
| `$receiver(callback)` | Register a message handler |
| `send(target, message)` | Send a message to an actor |
| `$delay(callback, seconds)` | Schedule a one-shot callback |
| `$clock(callback, interval)` | Schedule a repeating callback |
| `$stop()` | Terminate the current actor |
## Module Resolution
`use('path')` looks for modules in this order:
1. The current module's directory for `path.cm`
2. The mounted roots (the program's directory is mounted) for `path.cm`
3. Embedded/native modules if no script file is found
Modules compile to bytecode (`.o` files) and are cached for fast reloading.
## Example Games
Prosperon ships with example games:
- **Pong** — two-player paddle game
- **Snake** — growing snake on a grid
- **Tetris** — falling block puzzle
- **Chess** — full chess with move validation
- **Bunnymark** — sprite rendering benchmark
## Next Steps
- [Tutorial](tutorial/) — Build a game step by step
- [Entities](entities/) — The world and entity model
- [Rendering](rendering/) — How drawing works
- [Input](input/) — Handling player input
- [API Reference](api/) — Full module documentation

113
docs/rendering.md Normal file
View File

@@ -0,0 +1,113 @@
---
title: "Rendering"
type: docs
---
# Rendering
Prosperon's rendering is **data-driven**. You create sprites and other drawables, set their properties, and the engine handles drawing them. You never touch the GPU directly.
## The Model
Think of it like programming sprites on a classic console:
1. **Create** a sprite (or text, shape, tilemap, particle emitter)
2. **Set** its position, image, layer, and visual properties
3. The engine **draws** everything each frame, sorted and batched automatically
```javascript
var sprite = use('sprite')
var player = sprite({
image: "player.png",
pos: {x: 100, y: 200},
layer: 5
})
// Move it later
player.pos.x = 150
player.pos.y = 250
```
That's it. The sprite is registered with the engine when created and drawn every frame until destroyed.
## Coordinate System
There are two coordinate spaces:
**World space** is the game world. The camera's position determines what's visible. If the camera is at `[100, 100]`, that world coordinate is at the center of the screen. X+ goes right, Y+ goes up.
**HUD space** is the screen itself. `[0, 0]` is the bottom-left corner, `[camera.width, camera.height]` is the top-right. HUD elements don't move with the camera.
## Layers
Every drawable has a `layer` — an integer that determines draw order. Lower layers draw first (behind), higher layers draw last (in front).
Within a layer, objects can be sorted by different criteria:
- **explicit** (default) — engine may reorder for performance
- **y-sort** — sort by Y position, useful for top-down games where "lower" objects appear in front
## Planes
Drawables are organized into **planes**. A plane is a named group that can have its own resolution, camera, and effects. By default, everything goes into the `'default'` plane.
Planes render independently and composite onto the screen. This lets you have a game world at 320x240 pixel-art resolution and a HUD at native resolution, for example.
## Effects
Planes and groups of sprites can have effects applied:
- **Bloom** — threshold + multi-pass Gaussian blur
- **Mask** — stencil masking using other sprites as the mask shape
Effects are declared in the compositor config and applied automatically. You tag sprites with groups, then apply effects to those groups.
## Presentation Modes
The final composited frame is presented to the window in one of three modes:
| Mode | Behavior |
|------|----------|
| `stretch` | Fill the window, may distort |
| `letterbox` | Fit inside window, preserve aspect ratio, black bars |
| `integer_scale` | Scale by whole numbers only, nearest-neighbor, pixel-perfect |
## Camera
A camera defines the viewport into the world:
```javascript
var camera = use('camera')
var cam = camera.make({
pos: {x: 0, y: 0},
width: 320,
height: 240
})
```
The camera's `width` and `height` set the game's internal resolution. The `pos` determines what world coordinate is at the center of the screen.
## Drawable Types
| Type | Module | Description |
|------|--------|-------------|
| Sprite | `sprite` | Textured quad with transform, tint, UV mapping |
| Text | `text2d` | Rendered text with font, size, wrapping |
| Shape | `shape2d` | SDF-rendered rectangles, circles, ellipses, pills |
| Tilemap | `tilemap2d` | Grid of tiles on a single layer |
| Particles | `particles2d` | Particle emitter producing sprite-like particles |
| Line | `line2d` | Polylines and line segments as triangle meshes |
All drawable types share common properties: `pos`, `layer`, `plane`, `groups`, `visible`, `opacity`, `tint`.
## How It Works Internally
For those curious about the pipeline (you don't need to know this to use Prosperon):
1. Drawables register with `film2d` — the internal sprite registry
2. Each frame, the **compositor** queries film2d for all visible drawables
3. Drawables are sorted by layer, batched by texture/material
4. Effect groups are rendered to intermediate targets, effects applied
5. Planes composite to the screen via the **SDL GPU backend**
6. The backend uses Metal, Vulkan, or DirectX depending on platform

48
docs/resources.md Normal file
View File

@@ -0,0 +1,48 @@
---
title: "Resources"
type: docs
---
# Resources
Prosperon uses string paths to reference assets — images, sounds, fonts, scripts. The resource system resolves these paths and caches loaded assets.
## Module Resolution
`use('path')` loads a module by searching for `path.cm` in this order:
1. The current module's directory
2. Mounted roots (the program's directory is always mounted)
3. Embedded/native modules if no script file is found
Modules compile to bytecode (`.o` files) and are cached after first load. Subsequent `use()` calls return the cached value.
## Asset Resolution
When you reference an asset like `"player.png"`, the resource system searches mounted paths for a match. Assets should generally be referenced **without** a file extension — the engine tries appropriate extensions based on context:
- **Images:** png, qoi, gif, jpg, jpeg, ase
- **Sounds:** wav, ogg, mp3
- **Fonts:** ttf
Omitting extensions lets the engine swap optimized formats transparently. If you specify an extension, only that exact file is searched for.
## Caching
All loaded assets are cached. When two sprites reference the same image path, they share one GPU texture. Sound data, fonts, and bytecode are similarly deduplicated.
## Mounts
Prosperon uses a virtual filesystem. The write directory is set to the folder the engine runs from. That same folder is mounted as a read directory, along with the executable itself (which contains a zip archive of core engine assets).
Additional folders or zip archives can be mounted, allowing mods to override specific files:
```javascript
// A mod zip containing sprites/bug.png will override the game's bug.png
```
Because all asset references are string paths resolved through the mount system, modding is straightforward — mount a new archive and its files take priority.
## .prosperonignore
A `.prosperonignore` file in a mounted directory excludes files from resolution, similar to `.gitignore`. This is useful for keeping source assets (e.g., Aseprite working files) out of the build.

225
docs/tutorial.md Normal file
View File

@@ -0,0 +1,225 @@
---
title: "Tutorial"
type: docs
---
# A Tutorial Introduction
This tutorial walks through building a simple game from scratch. By the end, you'll understand modules, programs, rendering, and input.
## How Prosperon Starts
Prosperon runs a `.ce` program file as the root actor. This is your game's entry point. There is no `config.js` or `main.js` — everything is modules you import with `use()`.
## Step 1: A Window
Create `game.ce`:
```javascript
var core = use('core')
core.start({
title: "My Game",
width: 1280,
height: 720
})
```
Run it with `prosperon game.ce`. You get a window. `core.start()` initializes the GPU backend, opens the window, and starts the main loop.
## Step 2: Drawing a Sprite
Sprites are the fundamental drawable. Create one and it appears on screen — you don't issue draw calls.
```javascript
var core = use('core')
var sprite = use('sprite')
var compositor = use('compositor')
var camera = use('camera')
var cam = camera.make({
pos: {x: 0, y: 0},
width: 320,
height: 240
})
var player = sprite({
image: "player.png",
pos: {x: 0, y: 0},
width: 32,
layer: 5
})
var plan = compositor.compile({
planes: [{
name: 'game',
plane: 'default',
resolution: {width: 320, height: 240},
camera: cam,
presentation: 'integer_scale'
}]
})
core.start({
title: "Sprite Test",
width: 1280,
height: 720,
render: function() {
return compositor.execute(plan)
}
})
```
Key ideas:
- `sprite()` creates a sprite and registers it with the engine automatically
- The **compositor** describes how to render the scene: which planes, at what resolution, with what camera
- Your `render` callback returns the compositor result — the engine sends it to the GPU
## Step 3: Moving the Sprite
Add an `update` callback to move things each frame:
```javascript
core.start({
title: "Moving Sprite",
width: 1280,
height: 720,
update: function(dt) {
player.pos.x += 60 * dt
},
render: function() {
return compositor.execute(plan)
}
})
```
`dt` is the time elapsed since the last frame, in seconds. Multiply speeds by `dt` for frame-rate-independent movement.
## Step 4: Handling Input
Use the `input` module to map physical keys to named actions, then route those actions to your game objects:
```javascript
var input = use('input')
input.configure({
action_map: {
move_left: ['a', 'left'],
move_right: ['d', 'right'],
jump: ['space']
}
})
var player_entity = {
on_input: function(action, data) {
if (action == 'move_left' && data.pressed) {
player.pos.x -= 16
}
if (action == 'move_right' && data.pressed) {
player.pos.x += 16
}
}
}
var p1 = input.player1()
p1.possess(player_entity)
```
The input system handles keyboard, gamepad, and touch. You define actions once and bind them to any physical input.
## Step 5: Adding Sound
```javascript
var sound = use('sound')
sound.play("jump.wav")
```
Sounds are loaded by path. The engine caches audio data so loading the same sound twice reuses the existing data.
## Step 6: Multiple Planes
Real games often need separate rendering layers — pixel art at low res, HUD at native res:
```javascript
var plan = compositor.compile({
clear: {r: 0.1, g: 0.1, b: 0.2, a: 1},
planes: [
{
name: 'game',
plane: 'default',
resolution: {width: 320, height: 240},
camera: cam,
layer_sort: {'5': 'y'},
presentation: 'integer_scale'
},
{
name: 'hud',
plane: 'hud',
resolution: {width: 1280, height: 720},
camera: hud_cam,
presentation: 'stretch'
}
]
})
```
Sprites belong to a plane via their `plane` property. Each plane renders at its own resolution with its own camera, then composites onto the screen in order.
## Step 7: Entities
For anything more than a few sprites, use the entity system. Define entity types as modules, create instances with overrides:
```javascript
// entities/coin.cm
var sprite = use('sprite')
return {
value: 1,
image: "coin.png",
init: function() {
this.sprite = sprite({
image: this.image,
pos: this.pos,
width: 16,
layer: 3
})
},
on_destroy: function() {
this.sprite.destroy()
}
}
```
```javascript
// In your game program
var world = use('world')
var coin_proto = use('entities/coin')
var c = world.add_entity(coin_proto, {
pos: {x: 50, y: 80},
value: 5
})
```
The entity's `init()` runs after overrides are applied. The prototype defines every valid field with a default — it is the schema.
## Putting It Together
A complete minimal game has:
1. A `.ce` program that imports modules and calls `core.start()`
2. Sprite and entity modules (`.cm`) that define game objects
3. A compositor config that describes the rendering setup
4. Input configuration that maps keys to actions
The engine does the rest: batching sprites, sorting layers, managing GPU resources, polling events, scheduling frames.
## Next Steps
- [Graphics](../graphics/) — Sprites, text, shapes, tilemaps
- [Compositor](../ops/) — Planes, effects, presentation modes
- [Input](../input/) — Actions, players, control stacks
- [Entities](../entities/) — The entity model and world system

260
draw2d.cm
View File

@@ -1,251 +1,13 @@
var math = use('math')
var color = use('color')
var gamestate = use('gamestate')
var sprite = use('sprite')
var tilemap = use('tilemap2d')
var text = use('text2d')
var shape = use('shape2d')
var anim = use('anim2d')
var draw = {}
var current_list = []
// Clear current list
draw.clear = function() {
current_list = []
return {
sprite,
tilemap,
text,
shape,
anim
}
// Get commands from current list
draw.get_commands = function() {
return current_list
}
// Helper to add a command
function add_command(type, data) {
data.cmd = type
current_list.push(data)
}
// Default geometry definitions
var ellipse_def = {
start: 0,
end: 1,
mode: 'fill',
thickness: 1,
}
var line_def = {
thickness: 1,
cap:"butt",
}
var rect_def = {
thickness:1,
radius: 0
}
var image_info = {
tile_x: false,
tile_y: false,
flip_x: false,
flip_y: false,
mode: 'linear'
}
var circle_def = {
inner_radius:1, // percentage: 1 means filled circle
start:0,
end: 1,
}
// Drawing functions
draw.point = function(pos, size, opt = {}, material) {
add_command("draw_point", {
pos: pos,
size: size,
opt: opt,
material: material
})
}
draw.ellipse = function(pos, radii, defl, material) {
var opt = defl ? {...ellipse_def, ...defl} : ellipse_def
if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1])
add_command("draw_ellipse", {
pos: pos,
radii: radii,
opt: opt,
material: material
})
}
draw.line = function(points, defl, material)
{
var opt = defl ? {...line_def, ...defl} : line_def
add_command("draw_line", {
points: points,
opt: opt,
material: material
})
}
draw.cross = function render_cross(pos, size, defl, material) {
var a = [pos.add([0, size]), pos.add([0, -size])]
var b = [pos.add([size, 0]), pos.add([-size, 0])]
draw.line(a, defl, material)
draw.line(b, defl, material)
}
draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, defl, material) {
var dir = math.norm(end.sub(start))
var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end]
var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end]
draw.line([start, end], defl, material)
draw.line(wing1, defl, material)
draw.line(wing2, defl, material)
}
draw.rectangle = function render_rectangle(rect, defl, material = {color:{r:1,g:1,b:1,a:1}}) {
var opt = defl ? {...rect_def, ...defl} : rect_def
add_command("draw_rect", {
rect,
opt,
material
})
}
var slice9_info = {
tile_top:true,
tile_bottom:true,
tile_left:true,
tile_right:true,
tile_center_x:true,
tile_center_right:true,
}
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, material) {
if (!image) throw Error('Need an image to render.')
add_command("draw_slice9", {
image,
rect,
slice,
info,
material
})
}
draw.image = function image(image, rect, scale = {x:1,y:1}, anchor, shear, info, material) {
if (!rect) throw Error('Need rectangle to render image.')
if (!image) throw Error('Need an image to render.')
if (!('x' in rect && 'y' in rect)) throw Error('Must provide X and Y for image.')
add_command("draw_image", {
image,
rect,
scale,
anchor,
shear,
info,
material
})
}
draw.circle = function render_circle(pos, radius, defl, material) {
draw.ellipse(pos, [radius,radius], defl, material)
}
// wrap is the width before wrapping
// config is any additional config to pass to the text renderer
var text_base_config = {
align: 'left', // left, right, center, justify
break: 'word', // word, character
}
draw.text = function text(text, pos, font = 'fonts/c64.8', color = {r:1,g:1,b:1,a:1}, wrap = 0, config = {}) {
config.align ??= text_base_config.align
config.break ??= text_base_config.break
add_command("draw_text", {
text,
pos,
font,
wrap,
material: {color},
config
})
}
draw.grid = function grid(rect, spacing, thickness = 1, offset = {x: 0, y: 0}, material) {
if (!rect || rect.x == null || rect.y == null ||
rect.width == null || rect.height == null) {
throw Error('Grid requires rect with x, y, width, height')
}
if (!spacing || typeof spacing.x == 'undefined' || typeof spacing.y == 'undefined') {
throw Error('Grid requires spacing with x and y')
}
var left = rect.x
var right = rect.x + rect.width
var top = rect.y
var bottom = rect.y + rect.height
// Apply offset and align to grid
var start_x = Math.floor((left - offset.x) / spacing.x) * spacing.x + offset.x
var end_x = Math.ceil((right - offset.x) / spacing.x) * spacing.x + offset.x
var start_y = Math.floor((top - offset.y) / spacing.y) * spacing.y + offset.y
var end_y = Math.ceil((bottom - offset.y) / spacing.y) * spacing.y + offset.y
// Draw vertical lines
for (var x = start_x; x <= end_x; x += spacing.x) {
if (x >= left && x <= right) {
var line_top = Math.max(top, start_y)
var line_bottom = Math.min(bottom, end_y)
draw.line([[x, line_top], [x, line_bottom]], {thickness: thickness}, material)
}
}
// Draw horizontal lines
for (var y = start_y; y <= end_y; y += spacing.y) {
if (y >= top && y <= bottom) {
var line_left = Math.max(left, start_x)
var line_right = Math.min(right, end_x)
draw.line([[line_left, y], [line_right, y]], {thickness: thickness}, material)
}
}
}
draw.scissor = function(rect)
{
var screen_rect = null
if (rect && gamestate.camera) {
var bottom_left = gamestate.camera.world_to_window(rect.x, rect.y)
var top_right = gamestate.camera.world_to_window(rect.x + rect.width, rect.y + rect.height)
var screen_left = bottom_left.x
var screen_top = bottom_left.y
var screen_right = top_right.x
var screen_bottom = top_right.y
screen_rect = {
x: Math.round(screen_left),
y: Math.round(screen_top),
width: Math.round(screen_right - screen_left),
height: Math.round(screen_bottom - screen_top)
}
// TODO: must be a better way than manually inverting here. Some camera specific function.
var sensor = gamestate.camera.sensor()
screen_rect.y = sensor.height - screen_rect.y - screen_rect.height
}
current_list.push({
cmd: "scissor",
rect: screen_rect
})
}
draw.add_command = function(cmd)
{
current_list.push(cmd)
}
return draw

66
ease.cm
View File

@@ -1,3 +1,5 @@
var math = use('math/radians')
var Ease = {
linear(t) {
return t
@@ -19,16 +21,16 @@ function make_easing_fns(num) {
var obj = {}
obj.in = function (t) {
return Math.pow(t, num)
return math.power(t, num)
}
obj.out = function (t) {
return 1 - Math.pow(1 - t, num)
return 1 - math.power(1 - t, num)
}
var mult = Math.pow(2, num - 1)
var mult = math.power(2, num - 1)
obj.inout = function (t) {
return t < 0.5 ? mult * Math.pow(t, num) : 1 - Math.pow(-2 * t + 2, num) / 2
return t < 0.5 ? mult * math.power(t, num) : 1 - math.power(-2 * t + 2, num) / 2
}
return obj
@@ -41,10 +43,10 @@ Ease.quint = make_easing_fns(5)
Ease.expo = {
in(t) {
return t == 0 ? 0 : Math.pow(2, 10 * t - 10)
return t == 0 ? 0 : math.power(2, 10 * t - 10)
},
out(t) {
return t == 1 ? 1 : 1 - Math.pow(2, -10 * t)
return t == 1 ? 1 : 1 - math.power(2, -10 * t)
},
inout(t) {
return t == 0
@@ -52,8 +54,8 @@ Ease.expo = {
: t == 1
? 1
: t < 0.5
? Math.pow(2, 20 * t - 10) / 2
: (2 - Math.pow(2, -20 * t + 10)) / 2
? math.power(2, 20 * t - 10) / 2
: (2 - math.power(2, -20 * t + 10)) / 2
},
}
@@ -64,13 +66,19 @@ Ease.bounce = {
out(t) {
var n1 = 7.5625
var d1 = 2.75
if (t < 1 / d1) {
return n1 * t * t
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375
} else return n1 * (t -= 2.625 / d1) * t + 0.984375
var u = t
if (u < 1 / d1) {
return n1 * u * u
} else if (u < 2 / d1) {
u = u - 1.5 / d1
return n1 * u * u + 0.75
} else if (u < 2.5 / d1) {
u = u - 2.25 / d1
return n1 * u * u + 0.9375
} else {
u = u - 2.625 / d1
return n1 * u * u + 0.984375
}
},
inout(t) {
return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2
@@ -79,13 +87,13 @@ Ease.bounce = {
Ease.sine = {
in(t) {
return 1 - Math.cos((t * Math.PI) / 2)
return 1 - math.cosine((t * pi) / 2)
},
out(t) {
return Math.sin((t * Math.PI) / 2)
return math.sine((t * pi) / 2)
},
inout(t) {
return -(Math.cos(Math.PI * t) - 1) / 2
return -(math.cosine(pi * t) - 1) / 2
},
}
@@ -95,16 +103,16 @@ Ease.elastic = {
? 0
: t == 1
? 1
: -Math.pow(2, 10 * t - 10) *
Math.sin((t * 10 - 10.75) * this.c4)
: -math.power(2, 10 * t - 10) *
math.sine((t * 10 - 10.75) * this.c4)
},
out(t) {
return t == 0
? 0
: t == 1
? 1
: Math.pow(2, -10 * t) *
Math.sin((t * 10 - 0.75) * this.c4) +
: math.power(2, -10 * t) *
math.sine((t * 10 - 0.75) * this.c4) +
1
},
inout(t) {
@@ -113,13 +121,13 @@ Ease.elastic = {
: t == 1
? 1
: t < 0.5
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2 + 1
? -(math.power(2, 20 * t - 10) * math.sine((20 * t - 11.125) * this.c5)) / 2
: (math.power(2, -20 * t + 10) * math.sine((20 * t - 11.125) * this.c5)) / 2 + 1
},
}
Ease.elastic.c4 = (2 * Math.PI) / 3
Ease.elastic.c5 = (2 * Math.PI) / 4.5
Ease.elastic.c4 = (2 * pi) / 3
Ease.elastic.c5 = (2 * pi) / 4.5
Ease.zoom = {
// Creates a smooth zoom that maintains constant perceptual speed
@@ -128,9 +136,9 @@ Ease.zoom = {
return function(t) {
if (t == 0) return 0
if (t == 1) return 1
if (Math.abs(ratio - 1) < 0.001) return t
if (abs(ratio - 1) < 0.001) return t
// Position interpolation formula: (r^t - 1) / (r - 1)
return (Math.pow(ratio, t) - 1) / (ratio - 1)
return (math.power(ratio, t) - 1) / (ratio - 1)
}
},
// Exponential interpolation for zoom values
@@ -140,7 +148,7 @@ Ease.zoom = {
if (t == 0) return startZoom
if (t == 1) return endZoom
// Scale := Exp(LinearInterpolate(Ln(Scale1), Ln(Scale2), t))
return Math.exp(Math.log(startZoom) + t * (Math.log(endZoom) - Math.log(startZoom)))
return math.exp(math.ln(startZoom) + t * (math.ln(endZoom) - math.ln(startZoom)))
}
}
}

391
effects.cm Normal file
View File

@@ -0,0 +1,391 @@
// effects.cm - Effect Registry with Built-in Effect Recipes
//
// Effects are defined as recipes that produce abstract render passes.
// The compositor uses these recipes to build render plans.
// Backends implement the actual shader logic.
var effects = {}
// Effect registry
var _effects = {}
effects.register = function(name, deff) {
_effects[name] = deff
}
effects.get = function(name) {
return _effects[name]
}
effects.list = function() {
return array(_effects)
}
// Built-in effect: Bloom
effects.register('bloom', {
type: 'multi_pass',
requires_target: true,
params: {
threshold: {default: 0.8, type: 'float'},
intensity: {default: 1.0, type: 'float'},
blur_passes: {default: 3, type: 'int'}
},
build_passes: function(input, output, params, ctx) {
var passes = []
var size = ctx.target_size
// Threshold extraction
var thresh_target = ctx.alloc_target(size.width, size.height, 'bloom_thresh')
passes[] = {
type: 'shader',
shader: 'threshold',
input: input,
output: thresh_target,
uniforms: {
threshold: params.threshold != null ? params.threshold : 0.8,
intensity: params.intensity != null ? params.intensity : 1.0
}
}
// Blur ping-pong
var blur_a = ctx.alloc_target(size.width, size.height, 'bloom_blur_a')
var blur_b = ctx.alloc_target(size.width, size.height, 'bloom_blur_b')
var blur_src = thresh_target
var texel = {x: 1 / size.width, y: 1 / size.height}
var blur_count = params.blur_passes != null ? params.blur_passes : 3
var i = 0
for (i = 0; i < blur_count; i++) {
passes[] = {
type: 'shader',
shader: 'blur',
input: blur_src,
output: blur_a,
uniforms: {direction: {x: 2, y: 0}, texel_size: texel}
}
passes[] = {
type: 'shader',
shader: 'blur',
input: blur_a,
output: blur_b,
uniforms: {direction: {x: 0, y: 2}, texel_size: texel}
}
blur_src = blur_b
}
// Additive composite
passes[] = {
type: 'composite',
base: input,
overlay: blur_src,
output: output,
blend: 'add'
}
return passes
}
})
// Built-in effect: Mask
effects.register('mask', {
type: 'conditional',
requires_target: true,
params: {
source: {required: false}, // Legacy: direct handle reference
source_id: {required: false}, // New: ID string for film2d.get()
channel: {default: 'alpha'},
invert: {default: false},
soft: {default: false},
space: {default: 'local'}
},
build_passes: function(input, output, params, ctx) {
var passes = []
var size = ctx.target_size
// Check backend capabilities for stencil optimization
if (!params.soft && ctx.backend && ctx.backend.caps && ctx.backend.caps.has_stencil) {
// Could use stencil - but for now use texture approach
}
if (ctx.backend && ctx.backend.caps && !ctx.backend.caps.has_render_targets) {
// Can't do masks on this backend - just pass through
return [{type: 'blit', source: input, dest: output}]
}
// Resolve mask source
var mask_source = params.source
if (params.source_id && ctx.film2d) {
mask_source = ctx.film2d.get(params.source_id)
}
if (!mask_source) {
// No mask source - pass through
return [{type: 'blit', source: input, dest: output}]
}
// Render mask source to target
var mask_target = ctx.alloc_target(size.width, size.height, 'mask_src')
passes[] = {
type: 'render_subtree',
root: mask_source,
output: mask_target,
clear: {r: 0, g: 0, b: 0, a: 0},
space: params.space || 'local'
}
// Apply mask shader
passes[] = {
type: 'shader',
shader: 'mask',
inputs: [input, mask_target],
output: output,
uniforms: {
channel: params.channel == 'alpha' ? 0 : 1,
invert: params.invert ? 1 : 0
}
}
return passes
}
})
// Built-in effect: CRT
effects.register('crt', {
type: 'single_pass',
requires_target: true,
params: {
curvature: {default: 0.1},
scanline_intensity: {default: 0.3},
vignette: {default: 0.2}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'crt',
input: input,
output: output,
uniforms: {
curvature: params.curvature != null ? params.curvature : 0.1,
scanline_intensity: params.scanline_intensity != null ? params.scanline_intensity : 0.3,
vignette: params.vignette != null ? params.vignette : 0.2,
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
}
}]
}
})
// Built-in effect: Blur
effects.register('blur', {
type: 'multi_pass',
requires_target: true,
params: {
passes: {default: 2}
},
build_passes: function(input, output, params, ctx) {
var passes = []
var size = ctx.target_size
var texel = {x: 1 / size.width, y: 1 / size.height}
var blur_a = ctx.alloc_target(size.width, size.height, 'blur_a')
var blur_b = ctx.alloc_target(size.width, size.height, 'blur_b')
var src = input
var blur_count = params.passes != null ? params.passes : 2
var i = 0
for (i = 0; i < blur_count; i++) {
passes[] = {
type: 'shader',
shader: 'blur',
input: src,
output: blur_a,
uniforms: {direction: {x: 2, y: 0}, texel_size: texel}
}
passes[] = {
type: 'shader',
shader: 'blur',
input: blur_a,
output: blur_b,
uniforms: {direction: {x: 0, y: 2}, texel_size: texel}
}
src = blur_b
}
// Final blit to output
passes[] = {type: 'blit', source: src, dest: output}
return passes
}
})
// Built-in effect: Accumulator (motion blur / trails)
effects.register('accumulator', {
type: 'stateful',
requires_target: true,
params: {
decay: {default: 0.9}
},
build_passes: function(input, output, params, ctx) {
var size = ctx.target_size
var prev = ctx.get_persistent_target('accum_prev', size.width, size.height)
var curr = ctx.get_persistent_target('accum_curr', size.width, size.height)
return [
{
type: 'shader',
shader: 'accumulator',
inputs: [input, prev],
output: curr,
uniforms: {decay: params.decay != null ? params.decay : 0.9}
},
{type: 'blit', source: curr, dest: prev},
{type: 'blit', source: curr, dest: output}
]
}
})
// Built-in effect: Pixelate
effects.register('pixelate', {
type: 'single_pass',
requires_target: true,
params: {
pixel_size: {default: 4}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'pixelate',
input: input,
output: output,
uniforms: {
pixel_size: params.pixel_size != null ? params.pixel_size : 4,
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
}
}]
}
})
// Built-in effect: Color grading
effects.register('color_grade', {
type: 'single_pass',
requires_target: true,
params: {
brightness: {default: 0},
contrast: {default: 1},
saturation: {default: 1},
gamma: {default: 1}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'color_grade',
input: input,
output: output,
uniforms: {
brightness: params.brightness != null ? params.brightness : 0,
contrast: params.contrast != null ? params.contrast : 1,
saturation: params.saturation != null ? params.saturation : 1,
gamma: params.gamma != null ? params.gamma : 1
}
}]
}
})
// Built-in effect: Vignette
effects.register('vignette', {
type: 'single_pass',
requires_target: true,
params: {
intensity: {default: 0.3},
softness: {default: 0.5}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'vignette',
input: input,
output: output,
uniforms: {
intensity: params.intensity != null ? params.intensity : 0.3,
softness: params.softness != null ? params.softness : 0.5,
resolution: {width: ctx.target_size.width, height: ctx.target_size.height}
}
}]
}
})
// Built-in effect: Chromatic aberration
effects.register('chromatic', {
type: 'single_pass',
requires_target: true,
params: {
offset: {default: 0.005}
},
build_passes: function(input, output, params, ctx) {
return [{
type: 'shader',
shader: 'chromatic',
input: input,
output: output,
uniforms: {
offset: params.offset != null ? params.offset : 0.005
}
}]
}
})
// Built-in effect: Outline
effects.register('outline', {
type: 'single_pass',
requires_target: true,
params: {
color: {default: {r: 0, g: 0, b: 0, a: 1}},
width: {default: 1}
},
build_passes: function(input, output, params, ctx) {
var c = params.color || {r: 0, g: 0, b: 0, a: 1}
return [{
type: 'shader',
shader: 'outline',
input: input,
output: output,
uniforms: {
outline_color: c,
outline_width: params.width != null ? params.width : 1,
texel_size: {x: 1 / ctx.target_size.width, y: 1 / ctx.target_size.height}
}
}]
}
})
// Helper: Check if an effect requires a render target
effects.requires_target = function(effect_type) {
var deff = _effects[effect_type]
return deff ? (deff.requires_target || false) : false
}
// Helper: Get default params for an effect
effects.default_params = function(effect_type) {
var deff = _effects[effect_type]
if (!deff || !deff.params) return {}
var defaults = {}
arrfor(array(deff.params), k => {
if (deff.params[k].default != null) {
defaults[k] = deff.params[k].default
}
})
return defaults
}
// Helper: Validate effect params
effects.validate_params = function(effect_type, params) {
var deff = _effects[effect_type]
if (!deff || !deff.params) return true
arrfor(array(deff.params), k => {
if (deff.params[k].required && params[k] == null) {
return false
}
})
return true
}
return effects

87
emacs.cm Normal file
View File

@@ -0,0 +1,87 @@
// Emacs-style input handler
// Converts raw input with modifiers to standard emacs notation
// Valid keys for emacs bindings - only process these
var valid_keys = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'return', 'enter', 'space', 'escape', 'tab', 'backspace', 'delete',
'up', 'down', 'left', 'right', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
]
var action = {}
action.prefix_key = null // Current prefix key (C-x or C-c)
action.on_input = function(action_id, action_data)
{
if (!action_data.pressed) return
if (find(valid_keys, action_id) == null)
return
// Only process key events with modifiers or if we're waiting for a prefix continuation
if (!action_data.ctrl && !action_data.alt && !this.prefix_key)
return
var emacs_notation = ""
// Build emacs notation
if (action_data.ctrl)
emacs_notation += "C-"
if (action_data.alt)
emacs_notation += "M-"
// Convert action_id to emacs key notation
var key = action_id
if (length(key) == 1) {
// Single character keys
emacs_notation += lower(key)
} else {
// Handle special keys
if (key == 'return' || key == 'enter') {
emacs_notation += "RET"
} else if (key == 'space') {
emacs_notation += "SPC"
} else if (key == 'escape') {
emacs_notation += "ESC"
} else if (key == 'tab') {
emacs_notation += "TAB"
} else if (key == 'backspace') {
emacs_notation += "DEL"
} else if (key == 'delete') {
emacs_notation += "delete"
} else if (key == 'up') {
emacs_notation += "up"
} else if (key == 'down') {
emacs_notation += "down"
} else if (key == 'left') {
emacs_notation += "left"
} else if (key == 'right') {
emacs_notation += "right"
} else {
emacs_notation += key
}
}
// Handle prefix keys C-x and C-c
if (emacs_notation == "C-x" || emacs_notation == "C-c") {
this.prefix_key = emacs_notation
return
}
// If we have a prefix key, build the full command
var full_command = null
if (this.prefix_key) {
full_command = this.prefix_key + " " + emacs_notation
this.prefix_key = null // Reset prefix key
// scene.recurse(game.root, 'on_input', [full_command, action_data])
} else {
// scene.recurse(game.root, 'on_input', [emacs_notation, action_data])
}
}
return function()
{
var obj = meme(action)
return obj
}

Some files were not shown because too many files have changed in this diff Show More