Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/2d/text.js
Original file line number Diff line number Diff line change
@@ -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
})
});
}
});
31 changes: 29 additions & 2 deletions src/api/api_classes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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<JSFont>(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<JSFont>(c, new_target, js_font_class_id, font_path, size);
}

return Utils::create_js_instance<JSFont>(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
Expand Down
73 changes: 57 additions & 16 deletions src/api/api_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace HostApi {
JSDrawOptions options;
if (JS_IsObject(optionsObj)) {
Utils::try_get_opaque_property<JSColor>(ctx, optionsObj, "color", js_color_class_id, options.color);
Utils::try_get_opaque_property<JSVector2>(ctx, optionsObj, "origin", js_color_class_id, options.origin);
Utils::try_get_opaque_property<float>(ctx, optionsObj, "rotation", js_color_class_id, options.rotation);
Utils::try_get_opaque_property<bool>(ctx, optionsObj, "wireframe", js_color_class_id, options.wireframe);
Utils::try_get_opaque_property<JSVector2>(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);
Comment thread
burdockcascade marked this conversation as resolved.
}
return options;
}
Expand All @@ -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<JSVector2>(c, argv[0], js_vector2_class_id, {0, 0});
::DrawFPS(static_cast<int>(pos.x), static_cast<int>(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 {
Expand Down Expand Up @@ -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<JSFont>(ctx, optionsObj, "font", js_font_class_id, options.font);
Utils::try_get_opaque_property<JSColor>(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<JSVector2>(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<JSVector2>(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;
Comment thread
Copilot marked this conversation as resolved.
}, "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<JSVector2>(c, argv[0], js_vector2_class_id, {0, 0});
::DrawFPS(static_cast<int>(pos.x), static_cast<int>(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);
Expand Down
1 change: 1 addition & 0 deletions src/api/hostapi.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
30 changes: 29 additions & 1 deletion src/api/js_types.hpp
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#pragma once

#include <string>
#include <memory>
Comment thread
Copilot marked this conversation as resolved.
#include <cstdint>
#include <raylib.h>
#include "quickjs.h"
#include <quickjs.h>

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 {
Expand Down Expand Up @@ -48,11 +50,37 @@ namespace HostApi {
[[nodiscard]] constexpr operator Rectangle() const { return Rectangle { x, y, width, height }; }
};

struct JSFont {
std::shared_ptr<Font> 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<Font>(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;
bool wireframe = false;
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);
};

}
11 changes: 11 additions & 0 deletions src/api/js_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>(JS_ToBool(ctx, prop.get()));
return true;
}
return false;
}

// --- 1. Generic Getters and Setters ---

template <typename ClassType, typename FieldType, FieldType ClassType::*Member, JSClassID* ClassID>
Expand Down
20 changes: 20 additions & 0 deletions src/vectorjs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down