From 78e1ae885ac1d73fd158735744464670b0193200 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 1 Feb 2026 11:16:52 -0600 Subject: [PATCH] Add color support. --- assets/scene/initial.lua | 11 +- src/display/CMakeLists.txt | 10 +- src/display/color.csv | 3 +- src/display/{color.h => color3.h} | 0 src/script/module/display/modulecolor.c | 87 ++++++++++++++- src/script/module/display/modulecolor.h | 23 ++++ src/script/module/display/modulespritebatch.c | 18 +-- tools/display/color/csv/__main__.py | 104 ++++++++++++------ 8 files changed, 211 insertions(+), 45 deletions(-) rename src/display/{color.h => color3.h} (100%) diff --git a/assets/scene/initial.lua b/assets/scene/initial.lua index da94659..e58bfaf 100644 --- a/assets/scene/initial.lua +++ b/assets/scene/initial.lua @@ -2,15 +2,20 @@ module('spritebatch') module('time') module('camera') module('glm') +module('color') camera = cameraCreate(CAMERA_PROJECTION_TYPE_PERSPECTIVE) +color = colorBlue() + function sceneDispose() -- print('Disposing initial scene') end function sceneUpdate() - -- print('Updating initial scene') + color.r = 255 * (math.sin(TIME.time) + 1) * 0.5 + color.g = 255 * (math.sin(TIME.time + 2) + 1) * 0.5 + color.b = 255 * (math.sin(TIME.time + 4) + 1) * 0.5 end function sceneRender() @@ -19,8 +24,8 @@ function sceneRender() spriteBatchPush( nil, 0, 0, - 1, 1, - nil + 1, 2, + color ) spriteBatchFlush() diff --git a/src/display/CMakeLists.txt b/src/display/CMakeLists.txt index 5f321d6..9410ae1 100644 --- a/src/display/CMakeLists.txt +++ b/src/display/CMakeLists.txt @@ -37,4 +37,12 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp") DISPLAY_HEIGHT=272 DISPLAY_SIZE_DYNAMIC=0 ) -endif() \ No newline at end of file +endif() + +dusk_run_python( + dusk_color_defs + tools.display.color.csv + --csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv + --output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h +) +add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_color_defs) \ No newline at end of file diff --git a/src/display/color.csv b/src/display/color.csv index 586e061..62c2e1b 100644 --- a/src/display/color.csv +++ b/src/display/color.csv @@ -19,4 +19,5 @@ brown,0.6,0.4,0.2,1 pink,1,0.75,0.8,1 lime,0.75,1,0,1 navy,0,0,0.5,1 -teal,0,0.5,0.5,1 \ No newline at end of file +teal,0,0.5,0.5,1 +cornflower_blue,0.39,0.58,0.93,1 \ No newline at end of file diff --git a/src/display/color.h b/src/display/color3.h similarity index 100% rename from src/display/color.h rename to src/display/color3.h diff --git a/src/script/module/display/modulecolor.c b/src/script/module/display/modulecolor.c index b622a57..086187e 100644 --- a/src/script/module/display/modulecolor.c +++ b/src/script/module/display/modulecolor.c @@ -14,7 +14,45 @@ void moduleColor(scriptcontext_t *context) { assertNotNull(context, "Context cannot be NULL."); - scriptStructRegister(context, "color_mt", moduleColorGetter, NULL); + scriptStructRegister( + context, "color_mt", moduleColorGetter, moduleColorSetter + ); + + scriptContextRegFunc(context, "color", moduleColorFuncColor); + + scriptContextExec(context, COLOR_SCRIPT); +} + +int moduleColorFuncColor(lua_State *L) { + assertNotNull(L, "Lua state cannot be NULL."); + + scriptcontext_t *context = *(scriptcontext_t **)lua_getextraspace(L); + assertNotNull(context, "Script context cannot be NULL."); + + // Needs 4 channel uint8_t + if( + !lua_isnumber(L, 1) || !lua_isnumber(L, 2) || + !lua_isnumber(L, 3) || !lua_isnumber(L, 4) + ) { + return luaL_error(L, "color(r, g, b, a) requires four number arguments."); + } + + colorchannel8_t r = (colorchannel8_t)lua_tonumber(L, 1); + colorchannel8_t g = (colorchannel8_t)lua_tonumber(L, 2); + colorchannel8_t b = (colorchannel8_t)lua_tonumber(L, 3); + colorchannel8_t a = (colorchannel8_t)lua_tonumber(L, 4); + + // Create color_t as lua memory + color_t *color = (color_t*)lua_newuserdata(L, sizeof(color_t)); + color->r = r; + color->g = g; + color->b = b; + color->a = a; + + // Push color struct + scriptStructPushMetatable(context, "color_mt", color); + + return 1; } void moduleColorGetter( @@ -45,4 +83,51 @@ void moduleColorGetter( outValue->value.intValue = color->a; return; } +} + +void moduleColorSetter( + const scriptcontext_t *context, + const char_t *key, + void *structPtr, + const scriptvalue_t *inValue +) { + color_t *color = (color_t*)structPtr; + + if(stringCompare(key, "r") == 0) { + if(inValue->type == SCRIPT_VALUE_TYPE_INT) { + color->r = (colorchannel8_t)inValue->value.intValue; + } else if(inValue->type == SCRIPT_VALUE_TYPE_FLOAT) { + color->r = (colorchannel8_t)(inValue->value.floatValue); + } else { + assertUnreachable("Invalid type for color channel r"); + } + return; + } else if(stringCompare(key, "g") == 0) { + if(inValue->type == SCRIPT_VALUE_TYPE_INT) { + color->g = (colorchannel8_t)inValue->value.intValue; + } else if(inValue->type == SCRIPT_VALUE_TYPE_FLOAT) { + color->g = (colorchannel8_t)(inValue->value.floatValue); + } else { + assertUnreachable("Invalid type for color channel g"); + } + return; + } else if(stringCompare(key, "b") == 0) { + if(inValue->type == SCRIPT_VALUE_TYPE_INT) { + color->b = (colorchannel8_t)inValue->value.intValue; + } else if(inValue->type == SCRIPT_VALUE_TYPE_FLOAT) { + color->b = (colorchannel8_t)(inValue->value.floatValue); + } else { + assertUnreachable("Invalid type for color channel b"); + } + return; + } else if(stringCompare(key, "a") == 0) { + if(inValue->type == SCRIPT_VALUE_TYPE_INT) { + color->a = (colorchannel8_t)inValue->value.intValue; + } else if(inValue->type == SCRIPT_VALUE_TYPE_FLOAT) { + color->a = (colorchannel8_t)(inValue->value.floatValue); + } else { + assertUnreachable("Invalid type for color channel a"); + } + return; + } } \ No newline at end of file diff --git a/src/script/module/display/modulecolor.h b/src/script/module/display/modulecolor.h index c180534..5afbb28 100644 --- a/src/script/module/display/modulecolor.h +++ b/src/script/module/display/modulecolor.h @@ -15,6 +15,14 @@ */ void moduleColor(scriptcontext_t *context); +/** + * Lua function to create a color. + * + * @param L The Lua state. + * @return Number of return values. + */ +int moduleColorFuncColor(lua_State *L); + /** * Getter function for the color structure. * @@ -28,4 +36,19 @@ void moduleColorGetter( const char_t *key, const void *structPtr, scriptvalue_t *outValue +); + +/** + * Setter function for the color structure. + * + * @param context The script context. + * @param key The key to set. + * @param structPtr Pointer to the color structure. + * @param inValue Input value. + */ +void moduleColorSetter( + const scriptcontext_t *context, + const char_t *key, + void *structPtr, + const scriptvalue_t *inValue ); \ No newline at end of file diff --git a/src/script/module/display/modulespritebatch.c b/src/script/module/display/modulespritebatch.c index 4925edd..ece0652 100644 --- a/src/script/module/display/modulespritebatch.c +++ b/src/script/module/display/modulespritebatch.c @@ -45,13 +45,17 @@ int moduleSpriteBatchPush(lua_State *L) { return luaL_error(L, "Sprite coordinates must be numbers"); } - // Color (struct), must be Nil for now - if(!lua_isnil(L, 6)) { - return luaL_error(L, "Color parameter must be nil"); + // Color (struct) or nil for white + color_t *color = NULL; + if(lua_isuserdata(L, 6)) { + color = *(color_t**)lua_touserdata(L, 6); + assertNotNull(color, "Color struct cannot be NULL"); + } else if(lua_isnil(L, 6)) { + // Extrapolate later. + } else { + return luaL_error(L, "Sprite color must be a color struct or nil"); } - // if(!lua_istable(L, 6)) { - // return luaL_error(L, "Color parameter must be a color struct"); - // } + // Optional UV min and maxes, defaults to 0,0 -> 1,1 float_t u0 = 0.0f; @@ -88,7 +92,7 @@ int moduleSpriteBatchPush(lua_State *L) { minY, maxX, maxY, - COLOR_MAGENTA, + color == NULL ? COLOR_WHITE : *color, u0, v0, u1, diff --git a/tools/display/color/csv/__main__.py b/tools/display/color/csv/__main__.py index b69fe43..dc5a5e4 100644 --- a/tools/display/color/csv/__main__.py +++ b/tools/display/color/csv/__main__.py @@ -17,48 +17,88 @@ outHeader += '#include "dusk.h"\n\n' outHeader += f"typedef float_t colorchannelf_t;\n" +colors = {} with open(args.csv, newline="", encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) # CSV must have id column. - if "id" not in reader.fieldnames: - raise Exception("CSV file must have 'id' column") + if "name" not in reader.fieldnames: + raise Exception("CSV file must have 'name' column") - # For each ID - inputIds = [] - inputIdValues = {} + # For each color, by name... for row in reader: - inputId = row["id"] - if inputId not in inputIds: - inputIds.append(inputId) + name = row["name"] + r = row["r"] + g = row["g"] + b = row["b"] + a = row["a"] if "a" in row and row["a"] != "" else "1.0" - # For each ID, create enum entry. - count = 0 - outHeader += "typedef enum {\n" - outHeader += f" INPUT_ACTION_NULL = {count},\n\n" - count += 1 - for inputId in inputIds: - inputIdValues[inputId] = count - outHeader += f" {csvIdToEnumName(inputId)} = {count},\n" - count += 1 - outHeader += f"\n INPUT_ACTION_COUNT = {count}\n" - outHeader += "} inputaction_t;\n\n" + # Ensure values are between 0.0 and 1.0 + for channelValue in (r, g, b, a): + fvalue = float(channelValue) + if fvalue < 0.0 or fvalue > 1.0: + raise Exception(f"Color channel value {channelValue} for color {name} is out of range (0.0 to 1.0)") - # Write IDs to char array. - outHeader += f"static const char_t* INPUT_ACTION_IDS[] = {{\n" - for inputId in inputIds: - outHeader += f" [{csvIdToEnumName(inputId)}] = \"{inputId}\",\n" - outHeader += f"}};\n\n" + colors[name] = (r, g, b, a) - # Lua Script - outHeader += f"static const char_t *INPUT_ACTION_SCRIPT = \n" - for inputId in inputIds: - # Reference the enum - outHeader += f" \"{csvIdToEnumName(inputId)} = {inputIdValues[inputId]}\\n\"\n" - outHeader += f";\n\n" +# Prep output header +outHeader = "#pragma once\n" +outHeader += '#include "dusk.h"\n\n' -# Write to output file. -os.makedirs(os.path.dirname(args.output), exist_ok=True) +# Typedefs for float and uin8t color channels +outHeader += f"typedef float_t colorchannelf_t;\n" +outHeader += f"typedef uint8_t colorchannel8_t;\n\n" + +# Typedefs for 3 and 4 channel colors in both float and uint8_t +outHeader += f"typedef struct {{ colorchannelf_t r, g, b; }} color3f_t;\n" +outHeader += f"typedef struct {{ colorchannelf_t r, g, b, a; }} color4f_t;\n" +outHeader += f"typedef struct {{ colorchannel8_t r, g, b; }} color3b_t;\n" +outHeader += f"typedef struct {{ colorchannel8_t r, g, b, a; }} color4b_t;\n" +outHeader += "typedef color4b_t color_t;\n\n"# Preferred format. + +outHeader += "#define color3f(r, g, b) ((color3f_t){ r, g, b })\n" +outHeader += "#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })\n" +outHeader += "#define color3b(r, g, b) ((color3b_t){ r, g, b })\n" +outHeader += "#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })\n\n" + +luaScript = "" + +# Define each color, in each format +for color_name, (r, g, b, a) in colors.items(): + outHeader += f"// Color: {color_name}\n" + + r8 = int(float(r) * 255) + g8 = int(float(g) * 255) + b8 = int(float(b) * 255) + a8 = int(float(a) * 255) + + macro_name = "COLOR_" + color_name.upper() + + outHeader += f"#define {macro_name}_4B color4b({r8}, {g8}, {b8}, {a8})\n" + outHeader += f"#define {macro_name}_3B color3b({r8}, {g8}, {b8})\n" + outHeader += f"#define {macro_name}_3F color3f({r}f, {g}f, {b}f)\n" + outHeader += f"#define {macro_name}_4F color4f({r}f, {g}f, {b}f, {a}f)\n" + outHeader += f"#define {macro_name} {macro_name}_4B\n\n" # Preferred format. + + nameSplit = color_name.split("_") + camelFirstLetter = "" + for part in nameSplit: + camelFirstLetter += part[0].upper() + part[1:].lower() + + luaName = "color" + camelFirstLetter + + luaScript += f"function {luaName}()\n" + luaScript += f" return color({r8}, {g8}, {b8}, {a8})\n" + luaScript += "end\n\n" + +outHeader += "// Lua color functions\n" +outHeader += "#define COLOR_SCRIPT \\\n" +# Stringify and put each line with a backslash +for line in luaScript.splitlines(): + outHeader += f' "{line}\\n" \\\n' +outHeader = outHeader.rstrip(" \\\n") + "\n" # Remove last backslash and add newline + +# Write to output file with open(args.output, "w", encoding="utf-8") as outFile: outFile.write(outHeader) \ No newline at end of file