diff --git a/examples/2d/text.js b/examples/2d/text.js new file mode 100644 index 0000000..f2b3c08 --- /dev/null +++ b/examples/2d/text.js @@ -0,0 +1,27 @@ +import {Application, Vector2, Palette, Font} from "vectorjs"; + +const screenWidth = 800; +const screenHeight = 600; +const fpsPos = new Vector2(screenWidth/2, screenHeight/2); +const origin = new Vector2(1, 1) + +const app = new Application(screenWidth, screenHeight,"Window"); +app.run({ + + onInit() { + this.customFont = new Font("examples/assets/AnonymousPro-Regular.ttf"); + }, + + onDraw(render) { + render.withLayer2D((ctx) => { + ctx.text.drawText(fpsPos, "this is a sample text", { + font: this.customFont, + color: Palette.RED, + fontSize: 24.0, + spacing: 2.0, + rotation: 25.0, + origin: origin + }) + }); + } +}); \ No newline at end of file diff --git a/src/api/api_classes.cpp b/src/api/api_classes.cpp index a80bee6..46f44db 100644 --- a/src/api/api_classes.cpp +++ b/src/api/api_classes.cpp @@ -56,8 +56,9 @@ namespace HostApi { Utils::ScopedJSValue render_obj(ctx, create_draw_render_object(ctx)); if (JS_IsFunction(ctx, on_init_func)) { - Utils::ScopedJSValue ret(ctx, JS_Call(ctx, on_init_func, user_app, 0, nullptr)); - if (JS_IsException(ret)) return JS_EXCEPTION; + if (Utils::ScopedJSValue ret(ctx, JS_Call(ctx, on_init_func, user_app, 0, nullptr)); JS_IsException(ret)) { + return JS_EXCEPTION; + } } while (!WindowShouldClose()) { @@ -183,11 +184,37 @@ namespace HostApi { }); } + static void register_font_class(JSContext* ctx, JSModuleDef* m) { + Utils::register_js_class(ctx, m, { + .name = "Font", + .class_id = js_font_class_id, + .finalizer = [](auto, JSValue val) { + delete Utils::get_opaque(val, js_font_class_id); + }, + .constructor = [](auto c, auto new_target, int argc, auto argv) -> JSValue { + if (argc < 1) return JS_ThrowTypeError(c, "Font requires at least a path argument"); + + auto font_path = Utils::js_to_std_string(c, argv[0]); + if (font_path.empty()) return JS_ThrowTypeError(c, "Font path must be a non-empty string"); + + if (argc >= 2) { + int32_t size = 0; + if (JS_ToInt32(c, &size, argv[1]) < 0) return JS_EXCEPTION; + if (size <= 0) return JS_ThrowRangeError(c, "Font size must be a positive integer"); + return Utils::create_js_instance(c, new_target, js_font_class_id, font_path, size); + } + + return Utils::create_js_instance(c, new_target, js_font_class_id, font_path); + } + }); + } + void register_hapi_classes(JSContext* ctx, JSModuleDef* m) { register_application_class(ctx, m); register_color_class(ctx, m); register_vector2_class(ctx, m); register_rectangle_class(ctx, m); + register_font_class(ctx, m); } } // namespace HostApi diff --git a/src/api/api_context.cpp b/src/api/api_context.cpp index 2f18a5c..2f7d25b 100644 --- a/src/api/api_context.cpp +++ b/src/api/api_context.cpp @@ -9,9 +9,9 @@ namespace HostApi { JSDrawOptions options; if (JS_IsObject(optionsObj)) { Utils::try_get_opaque_property(ctx, optionsObj, "color", js_color_class_id, options.color); - Utils::try_get_opaque_property(ctx, optionsObj, "origin", js_color_class_id, options.origin); - Utils::try_get_opaque_property(ctx, optionsObj, "rotation", js_color_class_id, options.rotation); - Utils::try_get_opaque_property(ctx, optionsObj, "wireframe", js_color_class_id, options.wireframe); + Utils::try_get_opaque_property(ctx, optionsObj, "origin", js_vector2_class_id, options.origin); + Utils::try_get_float_property(ctx, optionsObj, "rotation", options.rotation); + Utils::try_get_bool_property(ctx, optionsObj, "wireframe", options.wireframe); } return options; } @@ -20,18 +20,7 @@ namespace HostApi { return JS_NewObject(ctx); } - JSValue create_draw_render_object(JSContext* ctx) { - Utils::ScopedJSValue render2d_obj(ctx, JS_NewObject(ctx)); - - // --- FPS Binding --- - JS_SetPropertyStr(ctx, render2d_obj, "drawFPS", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { - if (argc < 1) return JS_ThrowTypeError(c, "drawFPS requires a Vector2 position argument"); - const auto pos = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); - ::DrawFPS(static_cast(pos.x), static_cast(pos.y)); - return JS_UNDEFINED; - }, "drawFPS", 1)); - - // --- Shapes Sub-Object --- + static JSValue create_shapes_object(JSContext* ctx) { Utils::ScopedJSValue shape_obj(ctx, JS_NewObject(ctx)); JS_SetPropertyStr(ctx, shape_obj, "drawPixel", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { @@ -93,7 +82,59 @@ namespace HostApi { return JS_UNDEFINED; }, "drawEllipse", 4)); - JS_SetPropertyStr(ctx, render2d_obj, "shapes", shape_obj.release()); + return shape_obj.release(); + } + + static JSTextOptions parse_text_options(JSContext* ctx, JSValueConst optionsObj) { + JSTextOptions options; + if (JS_IsObject(optionsObj)) { + Utils::try_get_opaque_property(ctx, optionsObj, "font", js_font_class_id, options.font); + Utils::try_get_opaque_property(ctx, optionsObj, "color", js_color_class_id, options.color); + Utils::try_get_float_property(ctx, optionsObj, "rotation", options.rotation); + Utils::try_get_float_property(ctx, optionsObj, "fontSize", options.fontSize); + Utils::try_get_float_property(ctx, optionsObj, "spacing", options.spacing); + Utils::try_get_opaque_property(ctx, optionsObj, "origin", js_vector2_class_id, options.origin); + } + return options; + } + + static JSValue create_text_object(JSContext* ctx) { + Utils::ScopedJSValue text_obj(ctx, JS_NewObject(ctx)); + JS_SetPropertyStr(ctx, text_obj, "drawText", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 2) return JS_ThrowTypeError(c, "drawText requires position and text string arguments"); + const auto pos = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + + const char* txt_cstr = JS_ToCString(c, argv[1]); + if (!txt_cstr) return JS_EXCEPTION; + + JSTextOptions options = parse_text_options(c, argc > 2 ? argv[2] : JS_UNDEFINED); + Font fontToUse = GetFontDefault(); + if (options.font.font_ptr && options.font.font_ptr->texture.id != 0) { + fontToUse = *options.font.font_ptr; + } + ::DrawTextPro(fontToUse, txt_cstr, pos, options.origin, options.rotation, options.fontSize, options.spacing, options.color); + JS_FreeCString(c, txt_cstr); + return JS_UNDEFINED; + }, "drawText", 3)); + + return text_obj.release(); + } + + JSValue create_draw_render_object(JSContext* ctx) { + + Utils::ScopedJSValue render2d_obj(ctx, JS_NewObject(ctx)); + + // Add FPS + JS_SetPropertyStr(ctx, render2d_obj, "drawFPS", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 1) return JS_ThrowTypeError(c, "drawFPS requires a Vector2 position argument"); + const auto pos = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + ::DrawFPS(static_cast(pos.x), static_cast(pos.y)); + return JS_UNDEFINED; + }, "drawFPS", 1)); + + // Add Sub Objects + JS_SetPropertyStr(ctx, render2d_obj, "shapes", create_shapes_object(ctx)); + JS_SetPropertyStr(ctx, render2d_obj, "text", create_text_object(ctx)); // --- Layer Wrappers --- const JSValue render_obj = JS_NewObject(ctx); diff --git a/src/api/hostapi.hpp b/src/api/hostapi.hpp index f02d65e..fddeb00 100644 --- a/src/api/hostapi.hpp +++ b/src/api/hostapi.hpp @@ -23,6 +23,7 @@ namespace HostApi { JS_AddModuleExport(ctx, m, "Vector2"); JS_AddModuleExport(ctx, m, "Rectangle"); JS_AddModuleExport(ctx, m, "Palette"); + JS_AddModuleExport(ctx, m, "Font"); JS_AddModuleExport(ctx, m, "Info"); JS_AddModuleExport(ctx, m, "ConfigFlags"); } diff --git a/src/api/js_types.hpp b/src/api/js_types.hpp index eea77da..918b4ad 100644 --- a/src/api/js_types.hpp +++ b/src/api/js_types.hpp @@ -1,15 +1,17 @@ #pragma once #include +#include #include #include -#include "quickjs.h" +#include namespace HostApi { inline JSClassID js_color_class_id; inline JSClassID js_vector2_class_id; inline JSClassID js_rectangle_class_id; + inline JSClassID js_font_class_id; inline JSClassID js_application_class_id; struct JSApplication { @@ -48,6 +50,23 @@ namespace HostApi { [[nodiscard]] constexpr operator Rectangle() const { return Rectangle { x, y, width, height }; } }; + struct JSFont { + std::shared_ptr font_ptr; + JSFont() = default; + explicit JSFont(const std::string& path, const int baseSize = 64) { + const Font f = LoadFontEx(path.c_str(), baseSize, nullptr, 0); + if (f.texture.id != 0) { + SetTextureFilter(f.texture, TEXTURE_FILTER_BILINEAR); + } + font_ptr = std::shared_ptr(new Font(f), [](const Font* pf) { + if (pf->texture.id != 0) { + UnloadFont(*pf); + } + delete pf; + }); + } + }; + struct JSDrawOptions { JSColor color = JSColor(BLACK); float rotation = 0.0f; @@ -55,4 +74,13 @@ namespace HostApi { JSVector2 origin = JSVector2(0, 0); }; + struct JSTextOptions { + JSFont font; + JSColor color = JSColor(BLACK); + float rotation = 0.0f; + float fontSize = 24.0f; + float spacing = 1.0f; + JSVector2 origin = JSVector2(0, 0); + }; + } \ No newline at end of file diff --git a/src/api/js_utils.hpp b/src/api/js_utils.hpp index 9d2e292..6c1f304 100644 --- a/src/api/js_utils.hpp +++ b/src/api/js_utils.hpp @@ -101,6 +101,17 @@ namespace HostApi::Utils { return false; } + inline bool try_get_bool_property(JSContext* ctx, JSValueConst obj, const char* prop_name, bool& out_val) { + if (!JS_IsObject(obj)) return false; + + ScopedJSValue prop(ctx, JS_GetPropertyStr(ctx, obj, prop_name)); + if (!JS_IsUndefined(prop.get()) && JS_IsBool(prop.get())) { + out_val = static_cast(JS_ToBool(ctx, prop.get())); + return true; + } + return false; + } + // --- 1. Generic Getters and Setters --- template diff --git a/src/vectorjs.d.ts b/src/vectorjs.d.ts index 4992900..09cb1d9 100644 --- a/src/vectorjs.d.ts +++ b/src/vectorjs.d.ts @@ -97,6 +97,26 @@ declare module "vectorjs" { withLayer2D(callback: (layer: RenderContext) => void): void; } + /** + * Custom font resource loaded from a path. + */ + export class Font { + constructor(path: string); + constructor(path: string, baseSize: number); + } + + /** + * Custom options configuring text rendering layout and styling. + */ + export interface TextOptions { + readonly font?: Font; + readonly color?: Color; + readonly rotation?: number; + readonly fontSize?: number; + readonly spacing?: number; + readonly origin?: Vector2; + } + /** * Context parameter exposed to the `onUpdate` hook loop. */