Add a few more mesh types

This commit is contained in:
2026-04-14 08:38:50 -05:00
parent 378227c377
commit 0b570b5fd6
23 changed files with 875 additions and 83 deletions
+8 -8
View File
@@ -165,7 +165,7 @@ static errorret_t assetFileLineReaderAppend(
}
/* reserve room for optional NUL terminator */
if (reader->lineLength + srcLength >= reader->outBufferSize) {
if(reader->lineLength + srcLength >= reader->outBufferSize) {
errorThrow("Line length exceeds output buffer size.");
}
@@ -187,8 +187,8 @@ static ssize_t assetFileLineReaderFindNewline(const assetfilelinereader_t *reade
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
for (i = reader->bufferStart; i < reader->bufferEnd; ++i) {
if (reader->readBuffer[i] == '\n') {
for(i = reader->bufferStart; i < reader->bufferEnd; ++i) {
if(reader->readBuffer[i] == '\n') {
return (ssize_t)i;
}
}
@@ -209,7 +209,7 @@ errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) {
size_t unreadBytes = assetFileLineReaderUnreadBytes(reader);
/* If buffer is fully consumed, refill from start. */
if (unreadBytes == 0) {
if(unreadBytes == 0) {
reader->bufferStart = 0;
reader->bufferEnd = 0;
@@ -276,15 +276,15 @@ errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
reader->lineLength = 0;
for (;;) {
for(;;) {
ssize_t newlineIndex = assetFileLineReaderFindNewline(reader);
if (newlineIndex >= 0) {
if(newlineIndex >= 0) {
size_t chunkLength = (size_t)newlineIndex - reader->bufferStart;
errorret_t ret;
/* strip CR in CRLF */
if (chunkLength > 0 && reader->readBuffer[(size_t)newlineIndex - 1] == '\r') {
if(chunkLength > 0 && reader->readBuffer[(size_t)newlineIndex - 1] == '\r') {
chunkLength--;
}
@@ -299,7 +299,7 @@ errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
errorOk();
}
if (assetFileLineReaderUnreadBytes(reader) > 0) {
if(assetFileLineReaderUnreadBytes(reader) > 0) {
errorChain(assetFileLineReaderAppend(
reader,
assetFileLineReaderUnreadPtr(reader),
+11
View File
@@ -11,6 +11,10 @@
#include "display/spritebatch/spritebatch.h"
#include "display/mesh/quad.h"
#include "display/mesh/cube.h"
#include "display/mesh/sphere.h"
#include "display/mesh/plane.h"
#include "display/mesh/capsule.h"
#include "display/mesh/triprism.h"
#include "display/screen/screen.h"
#include "ui/ui.h"
#include "display/text/text.h"
@@ -35,8 +39,15 @@ errorret_t displayInit(void) {
&TEXTURE_WHITE, 4, 4,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
));
// Standard meshes
errorChain(quadInit());
errorChain(cubeInit());
errorChain(sphereInit());
errorChain(planeInit());
errorChain(capsuleInit());
errorChain(triPrismInit());
errorChain(frameBufferInitBackBuffer());
errorChain(spriteBatchInit());
errorChain(textInit());
+4
View File
@@ -9,4 +9,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
mesh.c
quad.c
cube.c
sphere.c
plane.c
capsule.c
triprism.c
)
+184
View File
@@ -0,0 +1,184 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "capsule.h"
#include "assert/assert.h"
mesh_t CAPSULE_MESH_SIMPLE;
meshvertex_t CAPSULE_MESH_SIMPLE_VERTICES[CAPSULE_VERTEX_COUNT];
errorret_t capsuleInit() {
vec3 center = { 0.0f, 0.0f, 0.0f };
capsuleBuffer(
CAPSULE_MESH_SIMPLE_VERTICES,
center,
0.5f,
0.5f,
CAPSULE_CAP_RINGS,
CAPSULE_SECTORS
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
errorChain(meshInit(
&CAPSULE_MESH_SIMPLE,
CAPSULE_PRIMITIVE_TYPE,
CAPSULE_VERTEX_COUNT,
CAPSULE_MESH_SIMPLE_VERTICES
));
errorOk();
}
void capsuleBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const float_t halfHeight,
const int32_t capRings,
const int32_t sectors
#if MESH_ENABLE_COLOR
, const color_t color
#endif
) {
assertNotNull(vertices, "Vertices cannot be NULL");
assertNotNull(center, "Center vector cannot be NULL");
const float_t cx = center[0];
const float_t cy = center[1];
const float_t cz = center[2];
const float_t sectorStep = 2.0f * (float_t)GLM_PI / (float_t)sectors;
int32_t vi = 0;
/* Helper macro: write one vertex. */
#if MESH_ENABLE_COLOR
#define CAP_VERT(px, py, pz, u, v) \
vertices[vi].color = color; \
vertices[vi].pos[0] = (px); \
vertices[vi].pos[1] = (py); \
vertices[vi].pos[2] = (pz); \
vertices[vi].uv[0] = (u); \
vertices[vi].uv[1] = (v); \
vi++;
#else
#define CAP_VERT(px, py, pz, u, v) \
vertices[vi].pos[0] = (px); \
vertices[vi].pos[1] = (py); \
vertices[vi].pos[2] = (pz); \
vertices[vi].uv[0] = (u); \
vertices[vi].uv[1] = (v); \
vi++;
#endif
/* ---- Top hemisphere ---- */
/* phi ranges from PI/2 (top pole) down to 0 (equator). */
const float_t capStep = (float_t)GLM_PI_2 / (float_t)capRings;
for(int32_t i = 0; i < capRings; i++) {
const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * capStep;
const float_t phi2 = (float_t)GLM_PI_2 - (float_t)(i + 1) * capStep;
const float_t ly1 = radius * sinf(phi1);
const float_t ly2 = radius * sinf(phi2);
const float_t lxz1 = radius * cosf(phi1);
const float_t lxz2 = radius * cosf(phi2);
/* UV: top cap occupies v in [0.5 + halfHeightFrac .. 1.0] — we use a
* simple per-band normalisation against the full height. */
const float_t v1 = 1.0f - (float_t)i / (float_t)(2 * capRings + 1);
const float_t v2 = 1.0f - (float_t)(i + 1) / (float_t)(2 * capRings + 1);
for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep;
const float_t t2 = (float_t)(j + 1) * sectorStep;
const float_t u1 = (float_t)j / (float_t)sectors;
const float_t u2 = (float_t)(j + 1) / (float_t)sectors;
const float_t x11 = lxz1 * cosf(t1), z11 = lxz1 * sinf(t1);
const float_t x12 = lxz1 * cosf(t2), z12 = lxz1 * sinf(t2);
const float_t x21 = lxz2 * cosf(t1), z21 = lxz2 * sinf(t1);
const float_t x22 = lxz2 * cosf(t2), z22 = lxz2 * sinf(t2);
const float_t y1off = cy + halfHeight + ly1;
const float_t y2off = cy + halfHeight + ly2;
CAP_VERT(cx+x11, y1off, cz+z11, u1, v1)
CAP_VERT(cx+x21, y2off, cz+z21, u1, v2)
CAP_VERT(cx+x12, y1off, cz+z12, u2, v1)
CAP_VERT(cx+x12, y1off, cz+z12, u2, v1)
CAP_VERT(cx+x21, y2off, cz+z21, u1, v2)
CAP_VERT(cx+x22, y2off, cz+z22, u2, v2)
}
}
/* ---- Cylindrical body ---- */
{
const float_t yTop = cy + halfHeight;
const float_t yBot = cy - halfHeight;
const float_t vTop = 1.0f - (float_t)capRings / (float_t)(2 * capRings + 1);
const float_t vBot = 1.0f - (float_t)(capRings + 1) / (float_t)(2 * capRings + 1);
for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep;
const float_t t2 = (float_t)(j + 1) * sectorStep;
const float_t u1 = (float_t)j / (float_t)sectors;
const float_t u2 = (float_t)(j + 1) / (float_t)sectors;
const float_t x1 = radius * cosf(t1), z1 = radius * sinf(t1);
const float_t x2 = radius * cosf(t2), z2 = radius * sinf(t2);
CAP_VERT(cx+x1, yTop, cz+z1, u1, vTop)
CAP_VERT(cx+x1, yBot, cz+z1, u1, vBot)
CAP_VERT(cx+x2, yTop, cz+z2, u2, vTop)
CAP_VERT(cx+x2, yTop, cz+z2, u2, vTop)
CAP_VERT(cx+x1, yBot, cz+z1, u1, vBot)
CAP_VERT(cx+x2, yBot, cz+z2, u2, vBot)
}
}
// Bottom hemisphere
for(int32_t i = 0; i < capRings; i++) {
const float_t phi1 = -(float_t)i * capStep;
const float_t phi2 = -(float_t)(i + 1) * capStep;
const float_t ly1 = radius * sinf(phi1);
const float_t ly2 = radius * sinf(phi2);
const float_t lxz1 = radius * cosf(phi1);
const float_t lxz2 = radius * cosf(phi2);
const float_t v1 = 1.0f - (float_t)(capRings + 1 + i) / (float_t)(2 * capRings + 1);
const float_t v2 = 1.0f - (float_t)(capRings + 1 + i + 1) / (float_t)(2 * capRings + 1);
for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep;
const float_t t2 = (float_t)(j + 1) * sectorStep;
const float_t u1 = (float_t)j / (float_t)sectors;
const float_t u2 = (float_t)(j + 1) / (float_t)sectors;
const float_t x11 = lxz1 * cosf(t1), z11 = lxz1 * sinf(t1);
const float_t x12 = lxz1 * cosf(t2), z12 = lxz1 * sinf(t2);
const float_t x21 = lxz2 * cosf(t1), z21 = lxz2 * sinf(t1);
const float_t x22 = lxz2 * cosf(t2), z22 = lxz2 * sinf(t2);
const float_t y1off = cy - halfHeight + ly1;
const float_t y2off = cy - halfHeight + ly2;
CAP_VERT(cx+x11, y1off, cz+z11, u1, v1)
CAP_VERT(cx+x21, y2off, cz+z21, u1, v2)
CAP_VERT(cx+x12, y1off, cz+z12, u2, v1)
CAP_VERT(cx+x12, y1off, cz+z12, u2, v1)
CAP_VERT(cx+x21, y2off, cz+z21, u1, v2)
CAP_VERT(cx+x22, y2off, cz+z22, u2, v2)
}
}
#undef CAP_VERT
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "display/mesh/mesh.h"
#include "display/color.h"
#define CAPSULE_CAP_RINGS 4
#define CAPSULE_SECTORS 16
#define CAPSULE_VERTEX_COUNT ((2 * CAPSULE_CAP_RINGS + 1) * CAPSULE_SECTORS * 6)
#define CAPSULE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
extern mesh_t CAPSULE_MESH_SIMPLE;
extern meshvertex_t CAPSULE_MESH_SIMPLE_VERTICES[CAPSULE_VERTEX_COUNT];
/**
* Initializes the simple unit capsule mesh centered at (0,0,0) with radius 0.5
* and a cylindrical half-height of 0.5 (total height 2.0).
*
* @return Error for initialization of the capsule mesh.
*/
errorret_t capsuleInit();
/**
* Buffers a capsule (cylinder + two hemisphere caps) into the provided vertex
* array. The capsule's long axis is Y. Total vertex count is
* (2*capRings + 1) * sectors * 6.
*
* @param vertices Vertex array to write into.
* @param center Center position of the capsule.
* @param radius Radius of the cylinder and hemisphere caps.
* @param halfHeight Half the height of the cylindrical section only (caps
* extend an additional radius above/below).
* @param capRings Number of latitude rings per hemisphere cap.
* @param sectors Number of longitude segments around the circumference.
* @param color Color applied to all vertices.
*/
void capsuleBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const float_t halfHeight,
const int32_t capRings,
const int32_t sectors
#if MESH_ENABLE_COLOR
, const color_t color
#endif
);
+10 -3
View File
@@ -14,7 +14,12 @@ meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
errorret_t cubeInit() {
vec3 min = { 0.0f, 0.0f, 0.0f };
vec3 max = { 1.0f, 1.0f, 1.0f };
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max, COLOR_WHITE_4B);
cubeBuffer(
CUBE_MESH_SIMPLE_VERTICES, min, max
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
errorChain(meshInit(
&CUBE_MESH_SIMPLE,
CUBE_PRIMITIVE_TYPE,
@@ -45,8 +50,10 @@ errorret_t cubeInit() {
void cubeBuffer(
meshvertex_t *vertices,
const vec3 min,
const vec3 max,
const color_t color
const vec3 max
#if MESH_ENABLE_COLOR
, const color_t color
#endif
) {
assertNotNull(vertices, "Vertices cannot be NULL");
assertNotNull(min, "Min vector cannot be NULL");
+4 -2
View File
@@ -36,6 +36,8 @@ errorret_t cubeInit();
void cubeBuffer(
meshvertex_t *vertices,
const vec3 min,
const vec3 max,
const color_t color
const vec3 max
#if MESH_ENABLE_COLOR
, const color_t color
#endif
);
+116
View File
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "plane.h"
#include "assert/assert.h"
mesh_t PLANE_MESH_SIMPLE;
meshvertex_t PLANE_MESH_SIMPLE_VERTICES[PLANE_VERTEX_COUNT];
errorret_t planeInit() {
vec3 min = { 0.0f, 0.0f, 0.0f };
vec3 max = { 1.0f, 0.0f, 1.0f };
vec2 uvMin = { 0.0f, 0.0f };
vec2 uvMax = { 1.0f, 1.0f };
planeBuffer(
PLANE_MESH_SIMPLE_VERTICES,
PLANE_AXIS_XZ,
min,
max
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
, uvMin,
uvMax
);
errorChain(meshInit(
&PLANE_MESH_SIMPLE,
PLANE_PRIMITIVE_TYPE,
PLANE_VERTEX_COUNT,
PLANE_MESH_SIMPLE_VERTICES
));
errorOk();
}
/* Helper macro to write one vertex. */
#if MESH_ENABLE_COLOR
#define PLANE_VERT(i, px, py, pz, u, v) \
vertices[i].color = color; \
vertices[i].pos[0] = (px); \
vertices[i].pos[1] = (py); \
vertices[i].pos[2] = (pz); \
vertices[i].uv[0] = (u); \
vertices[i].uv[1] = (v);
#else
#define PLANE_VERT(i, px, py, pz, u, v) \
vertices[i].pos[0] = (px); \
vertices[i].pos[1] = (py); \
vertices[i].pos[2] = (pz); \
vertices[i].uv[0] = (u); \
vertices[i].uv[1] = (v);
#endif
void planeBuffer(
meshvertex_t *vertices,
const planeaxis_t axis,
const vec3 min,
const vec3 max
#if MESH_ENABLE_COLOR
, const color_t color
#endif
, const vec2 uvMin,
const vec2 uvMax
) {
assertNotNull(vertices, "Vertices cannot be NULL");
assertNotNull(min, "Min vector cannot be NULL");
assertNotNull(max, "Max vector cannot be NULL");
assertNotNull(uvMin, "uvMin cannot be NULL");
assertNotNull(uvMax, "uvMax cannot be NULL");
const float_t u0 = uvMin[0], u1 = uvMax[0];
const float_t v0 = uvMin[1], v1 = uvMax[1];
switch (axis) {
case PLANE_AXIS_XY: {
/* Flat in XY at z = min[2]; spans X and Y. */
const float_t z = min[2];
PLANE_VERT(0, min[0], min[1], z, u0, v0)
PLANE_VERT(1, max[0], min[1], z, u1, v0)
PLANE_VERT(2, max[0], max[1], z, u1, v1)
PLANE_VERT(3, min[0], min[1], z, u0, v0)
PLANE_VERT(4, max[0], max[1], z, u1, v1)
PLANE_VERT(5, min[0], max[1], z, u0, v1)
break;
}
case PLANE_AXIS_XZ: {
/* Flat in XZ at y = min[1]; spans X and Z. */
const float_t y = min[1];
PLANE_VERT(0, min[0], y, min[2], u0, v0)
PLANE_VERT(1, max[0], y, min[2], u1, v0)
PLANE_VERT(2, max[0], y, max[2], u1, v1)
PLANE_VERT(3, min[0], y, min[2], u0, v0)
PLANE_VERT(4, max[0], y, max[2], u1, v1)
PLANE_VERT(5, min[0], y, max[2], u0, v1)
break;
}
case PLANE_AXIS_YZ: {
/* Flat in YZ at x = min[0]; spans Y and Z. */
const float_t x = min[0];
PLANE_VERT(0, x, min[1], min[2], u0, v0)
PLANE_VERT(1, x, max[1], min[2], u1, v0)
PLANE_VERT(2, x, max[1], max[2], u1, v1)
PLANE_VERT(3, x, min[1], min[2], u0, v0)
PLANE_VERT(4, x, max[1], max[2], u1, v1)
PLANE_VERT(5, x, min[1], max[2], u0, v1)
break;
}
}
#undef PLANE_VERT
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "display/mesh/mesh.h"
#include "display/color.h"
#define PLANE_VERTEX_COUNT 6
#define PLANE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
/** Which axis the plane's normal points along. */
typedef enum {
PLANE_AXIS_XY = 0, /**< Flat in XY, normal along +Z (billboard/wall face). */
PLANE_AXIS_XZ = 1, /**< Flat in XZ, normal along +Y (ground/floor plane). */
PLANE_AXIS_YZ = 2, /**< Flat in YZ, normal along +X (side wall). */
} planeaxis_t;
extern mesh_t PLANE_MESH_SIMPLE;
extern meshvertex_t PLANE_MESH_SIMPLE_VERTICES[PLANE_VERTEX_COUNT];
/**
* Initializes the simple unit XZ plane mesh (ground plane) from (0,0,0) to
* (1,0,1).
*
* @return Error for initialization of the plane mesh.
*/
errorret_t planeInit();
/**
* Buffers an axis-aligned plane into the provided vertex array.
* Writes PLANE_VERTEX_COUNT (6) vertices (two triangles, CCW winding).
*
* The min/max corners fully describe the plane in 3D space. The axis enum
* controls which dimension is treated as the "depth" (normal) axis:
* PLANE_AXIS_XY — spans X and Y, depth from min[2]/max[2] (uses min[2])
* PLANE_AXIS_XZ — spans X and Z, depth from min[1]/max[1] (uses min[1])
* PLANE_AXIS_YZ — spans Y and Z, depth from min[0]/max[0] (uses min[0])
*
* @param vertices Vertex array to write into (must hold PLANE_VERTEX_COUNT).
* @param axis Which axis the plane's normal points along.
* @param min Minimum XYZ corner.
* @param max Maximum XYZ corner.
* @param color Color applied to all vertices.
* @param uvMin Minimum UV coordinates.
* @param uvMax Maximum UV coordinates.
*/
void planeBuffer(
meshvertex_t *vertices,
const planeaxis_t axis,
const vec3 min,
const vec3 max
#if MESH_ENABLE_COLOR
, const color_t color
#endif
, const vec2 uvMin,
const vec2 uvMax
);
+6 -6
View File
@@ -77,13 +77,13 @@ void quadBuffer(
const float_t minY,
const float_t maxX,
const float_t maxY,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const float_t u0,
const float_t v0,
const float_t u1,
const float_t v1
#if MESH_ENABLE_COLOR
, const color_t color
#endif
) {
const float_t z = 0.0f; // Z coordinate for 2D rendering
assertNotNull(vertices, "Vertices cannot be NULL");
@@ -149,11 +149,11 @@ void quadBuffer3D(
meshvertex_t *vertices,
const vec3 min,
const vec3 max,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const vec2 uvMin,
const vec2 uvMax
#if MESH_ENABLE_COLOR
, const color_t color
#endif
) {
assertNotNull(vertices, "Vertices cannot be NULL");
assertNotNull(min, "Min vector cannot be NULL");
+6 -6
View File
@@ -42,13 +42,13 @@ void quadBuffer(
const float_t minY,
const float_t maxX,
const float_t maxY,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const float_t u0,
const float_t v0,
const float_t u1,
const float_t v1
#if MESH_ENABLE_COLOR
, const color_t color
#endif
);
/**
@@ -65,9 +65,9 @@ void quadBuffer3D(
meshvertex_t *vertices,
const vec3 min,
const vec3 max,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const vec2 uvMin,
const vec2 uvMax
#if MESH_ENABLE_COLOR
, const color_t color
#endif
);
+145
View File
@@ -0,0 +1,145 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "sphere.h"
#include "assert/assert.h"
mesh_t SPHERE_MESH_SIMPLE;
meshvertex_t SPHERE_MESH_SIMPLE_VERTICES[SPHERE_VERTEX_COUNT];
errorret_t sphereInit() {
vec3 center = { 0.0f, 0.0f, 0.0f };
sphereBuffer(
SPHERE_MESH_SIMPLE_VERTICES,
center,
0.5f,
SPHERE_STACKS,
SPHERE_SECTORS
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
errorChain(meshInit(
&SPHERE_MESH_SIMPLE,
SPHERE_PRIMITIVE_TYPE,
SPHERE_VERTEX_COUNT,
SPHERE_MESH_SIMPLE_VERTICES
));
errorOk();
}
void sphereBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const int32_t stacks,
const int32_t sectors
#if MESH_ENABLE_COLOR
, const color_t color
#endif
) {
assertNotNull(vertices, "Vertices cannot be NULL");
assertNotNull(center, "Center vector cannot be NULL");
const float_t stackStep = (float_t)GLM_PI / (float_t)stacks;
const float_t sectorStep = 2.0f * (float_t)GLM_PI / (float_t)sectors;
int32_t vi = 0;
for(int32_t i = 0; i < stacks; i++) {
/* Latitude angles: top of band -> bottom of band */
const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * stackStep;
const float_t phi2 = (float_t)GLM_PI_2 - (float_t)(i + 1) * stackStep;
const float_t y1 = radius * sinf(phi1);
const float_t y2 = radius * sinf(phi2);
const float_t xz1 = radius * cosf(phi1);
const float_t xz2 = radius * cosf(phi2);
const float_t v1 = 1.0f - (float_t)i / (float_t)stacks;
const float_t v2 = 1.0f - (float_t)(i + 1) / (float_t)stacks;
for(int32_t j = 0; j < sectors; j++) {
const float_t theta1 = (float_t)j * sectorStep;
const float_t theta2 = (float_t)(j + 1) * sectorStep;
const float_t x11 = xz1 * cosf(theta1);
const float_t z11 = xz1 * sinf(theta1);
const float_t x12 = xz1 * cosf(theta2);
const float_t z12 = xz1 * sinf(theta2);
const float_t x21 = xz2 * cosf(theta1);
const float_t z21 = xz2 * sinf(theta1);
const float_t x22 = xz2 * cosf(theta2);
const float_t z22 = xz2 * sinf(theta2);
const float_t u1 = (float_t)j / (float_t)sectors;
const float_t u2 = (float_t)(j + 1) / (float_t)sectors;
/* Triangle 1: top-left, bottom-left, top-right */
#if MESH_ENABLE_COLOR
vertices[vi].color = color;
#endif
vertices[vi].pos[0] = center[0] + x11;
vertices[vi].pos[1] = center[1] + y1;
vertices[vi].pos[2] = center[2] + z11;
vertices[vi].uv[0] = u1;
vertices[vi].uv[1] = v1;
vi++;
#if MESH_ENABLE_COLOR
vertices[vi].color = color;
#endif
vertices[vi].pos[0] = center[0] + x21;
vertices[vi].pos[1] = center[1] + y2;
vertices[vi].pos[2] = center[2] + z21;
vertices[vi].uv[0] = u1;
vertices[vi].uv[1] = v2;
vi++;
#if MESH_ENABLE_COLOR
vertices[vi].color = color;
#endif
vertices[vi].pos[0] = center[0] + x12;
vertices[vi].pos[1] = center[1] + y1;
vertices[vi].pos[2] = center[2] + z12;
vertices[vi].uv[0] = u2;
vertices[vi].uv[1] = v1;
vi++;
/* Triangle 2: top-right, bottom-left, bottom-right */
#if MESH_ENABLE_COLOR
vertices[vi].color = color;
#endif
vertices[vi].pos[0] = center[0] + x12;
vertices[vi].pos[1] = center[1] + y1;
vertices[vi].pos[2] = center[2] + z12;
vertices[vi].uv[0] = u2;
vertices[vi].uv[1] = v1;
vi++;
#if MESH_ENABLE_COLOR
vertices[vi].color = color;
#endif
vertices[vi].pos[0] = center[0] + x21;
vertices[vi].pos[1] = center[1] + y2;
vertices[vi].pos[2] = center[2] + z21;
vertices[vi].uv[0] = u1;
vertices[vi].uv[1] = v2;
vi++;
#if MESH_ENABLE_COLOR
vertices[vi].color = color;
#endif
vertices[vi].pos[0] = center[0] + x22;
vertices[vi].pos[1] = center[1] + y2;
vertices[vi].pos[2] = center[2] + z22;
vertices[vi].uv[0] = u2;
vertices[vi].uv[1] = v2;
vi++;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "display/mesh/mesh.h"
#include "display/color.h"
#define SPHERE_STACKS 8
#define SPHERE_SECTORS 16
#define SPHERE_VERTEX_COUNT (SPHERE_STACKS * SPHERE_SECTORS * 6)
#define SPHERE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
extern mesh_t SPHERE_MESH_SIMPLE;
extern meshvertex_t SPHERE_MESH_SIMPLE_VERTICES[SPHERE_VERTEX_COUNT];
/**
* Initializes the simple unit sphere mesh centered at (0,0,0) with radius 0.5.
*
* @return Error for initialization of the sphere mesh.
*/
errorret_t sphereInit();
/**
* Buffers a UV sphere into the provided vertex array.
* Writes stacks * sectors * 6 vertices (CCW winding).
*
* @param vertices Vertex array to write into (must hold stacks*sectors*6).
* @param center Center position of the sphere.
* @param radius Radius of the sphere.
* @param stacks Number of horizontal rings (latitude bands).
* @param sectors Number of vertical segments (longitude slices).
* @param color Color applied to all vertices.
*/
void sphereBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const int32_t stacks,
const int32_t sectors
#if MESH_ENABLE_COLOR
, const color_t color
#endif
);
+106
View File
@@ -0,0 +1,106 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "triprism.h"
#include "assert/assert.h"
mesh_t TRIPRISM_MESH_SIMPLE;
meshvertex_t TRIPRISM_MESH_SIMPLE_VERTICES[TRIPRISM_VERTEX_COUNT];
errorret_t triPrismInit() {
triPrismBuffer(
TRIPRISM_MESH_SIMPLE_VERTICES,
0.0f, 0.0f, /* p0: bottom-left */
1.0f, 0.0f, /* p1: bottom-right */
0.5f, 1.0f, /* p2: apex */
0.0f, 1.0f /* minZ, maxZ */
#if MESH_ENABLE_COLOR
, COLOR_WHITE_4B
#endif
);
errorChain(meshInit(
&TRIPRISM_MESH_SIMPLE,
TRIPRISM_PRIMITIVE_TYPE,
TRIPRISM_VERTEX_COUNT,
TRIPRISM_MESH_SIMPLE_VERTICES
));
errorOk();
}
void triPrismBuffer(
meshvertex_t *vertices,
const float_t x0, const float_t y0,
const float_t x1, const float_t y1,
const float_t x2, const float_t y2,
const float_t minZ,
const float_t maxZ
#if MESH_ENABLE_COLOR
, const color_t color
#endif
) {
assertNotNull(vertices, "Vertices cannot be NULL");
/* Helper macro: write one vertex then advance index. */
int32_t vi = 0;
#if MESH_ENABLE_COLOR
#define PRISM_VERT(px, py, pz, u, v) \
vertices[vi].color = color; \
vertices[vi].pos[0] = (px); \
vertices[vi].pos[1] = (py); \
vertices[vi].pos[2] = (pz); \
vertices[vi].uv[0] = (u); \
vertices[vi].uv[1] = (v); \
vi++;
#else
#define PRISM_VERT(px, py, pz, u, v) \
vertices[vi].pos[0] = (px); \
vertices[vi].pos[1] = (py); \
vertices[vi].pos[2] = (pz); \
vertices[vi].uv[0] = (u); \
vertices[vi].uv[1] = (v); \
vi++;
#endif
/* --- Front face (z = maxZ), CCW from +Z --- */
PRISM_VERT(x0, y0, maxZ, 0.0f, 0.0f)
PRISM_VERT(x1, y1, maxZ, 1.0f, 0.0f)
PRISM_VERT(x2, y2, maxZ, 0.5f, 1.0f)
/* --- Back face (z = minZ), CCW from -Z (reverse winding) --- */
PRISM_VERT(x2, y2, minZ, 0.5f, 1.0f)
PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f)
PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f)
/* --- Side face 0: edge p0->p1 --- */
PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f)
PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f)
PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f)
PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f)
PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f)
PRISM_VERT(x0, y0, maxZ, 0.0f, 1.0f)
/* --- Side face 1: edge p1->p2 --- */
PRISM_VERT(x1, y1, minZ, 0.0f, 0.0f)
PRISM_VERT(x2, y2, minZ, 1.0f, 0.0f)
PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f)
PRISM_VERT(x1, y1, minZ, 0.0f, 0.0f)
PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f)
PRISM_VERT(x1, y1, maxZ, 0.0f, 1.0f)
/* --- Side face 2: edge p2->p0 --- */
PRISM_VERT(x2, y2, minZ, 0.0f, 0.0f)
PRISM_VERT(x0, y0, minZ, 1.0f, 0.0f)
PRISM_VERT(x0, y0, maxZ, 1.0f, 1.0f)
PRISM_VERT(x2, y2, minZ, 0.0f, 0.0f)
PRISM_VERT(x0, y0, maxZ, 1.0f, 1.0f)
PRISM_VERT(x2, y2, maxZ, 0.0f, 1.0f)
#undef PRISM_VERT
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "display/mesh/mesh.h"
#include "display/color.h"
/**
* Vertex layout:
* 2 triangular end-caps (3 verts each) = 6
* 3 rectangular side faces (6 verts each) = 18
* Total = 24
*/
#define TRIPRISM_VERTEX_COUNT 24
#define TRIPRISM_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
extern mesh_t TRIPRISM_MESH_SIMPLE;
extern meshvertex_t TRIPRISM_MESH_SIMPLE_VERTICES[TRIPRISM_VERTEX_COUNT];
/**
* Initializes the simple unit triangular prism. The cross-section triangle has
* vertices (0,0), (1,0), (0.5,1) in XY, extruded from z=0 to z=1.
*
* @return Error for initialization of the triangular prism mesh.
*/
errorret_t triPrismInit();
/**
* Buffers a triangular prism into the provided vertex array.
* The triangular cross-section is defined by three 2D points in the XY plane;
* the prism is extruded along the Z axis between minZ and maxZ.
* Writes TRIPRISM_VERTEX_COUNT (24) vertices (CCW winding).
*
* @param vertices Vertex array to write into (must hold TRIPRISM_VERTEX_COUNT).
* @param x0,y0 First triangle vertex (XY).
* @param x1,y1 Second triangle vertex (XY).
* @param x2,y2 Third triangle vertex (XY).
* @param minZ Near Z extent of the prism.
* @param maxZ Far Z extent of the prism.
* @param color Color applied to all vertices.
*/
void triPrismBuffer(
meshvertex_t *vertices,
const float_t x0, const float_t y0,
const float_t x1, const float_t y1,
const float_t x2, const float_t y2,
const float_t minZ,
const float_t maxZ
#if MESH_ENABLE_COLOR
, const color_t color
#endif
);
+6 -6
View File
@@ -26,11 +26,11 @@ errorret_t screenInit() {
SCREEN.frameBufferMeshVertices,
0.0f, 0.0f,
1.0f, 1.0f,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
0.0f, 0.0f,
1.0f, 1.0f
#if MESH_ENABLE_COLOR
, COLOR_WHITE
#endif
);
errorChain(meshInit(
&SCREEN.frameBufferMesh,
@@ -363,11 +363,11 @@ errorret_t screenRender() {
SCREEN.frameBufferMeshVertices,
centerX - fbWidth * 0.5f, centerY + fbHeight * 0.5f, // top-left
centerX + fbWidth * 0.5f, centerY - fbHeight * 0.5f, // bottom-right
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
0.0f, 0.0f,
1.0f, 1.0f
#if MESH_ENABLE_COLOR
, COLOR_WHITE
#endif
);
frameBufferClear(
+3 -3
View File
@@ -69,10 +69,10 @@ errorret_t spriteBatchPush3D(
quadBuffer3D(
&SPRITEBATCH_VERTICES[vertexOffset],
min, max,
#if MESH_ENABLE_COLOR
color,
#endif
uv0, uv1
#if MESH_ENABLE_COLOR
, color
#endif
);
SPRITEBATCH.spriteCount++;
errorOk();
+2 -1
View File
@@ -18,8 +18,9 @@
#include "assert/assert.h"
#include "entity/entitymanager.h"
#include "game/game.h"
#include "display/mesh/quad.h"
#include "display/mesh/quad.h"
#include "display/mesh/capsule.h"
#include "asset/loader/display/assetmeshloader.h"
engine_t ENGINE;
@@ -171,7 +171,7 @@ void entityPositionDecompose(entityposition_t *pos) {
pos->rotation[1] = asinf(sinBeta);
float cosBeta = cosf(pos->rotation[1]);
if (fabsf(cosBeta) > 1e-6f) {
if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-r[2][1], r[2][2]);
pos->rotation[2] = atan2f(-r[1][0], r[0][0]);
} else {
+10 -10
View File
@@ -35,9 +35,9 @@ const DTF = (() => {
// When format is FORMAT_ALPHA and redAsAlpha is true, the red channel is
// used as the alpha value instead of the actual alpha channel.
function encode(width, height, rgbaData, format, redAsAlpha) {
if (format === undefined) format = FORMAT_RGBA;
if(format === undefined) format = FORMAT_RGBA;
const bpp = BPP[format];
if (bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`);
if(bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`);
const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData);
const buf = new ArrayBuffer(HEADER_SIZE + width * height * bpp);
@@ -53,7 +53,7 @@ const DTF = (() => {
bytes[12] = format;
let dst = HEADER_SIZE;
for (let i = 0; i < width * height; i++) {
for(let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
@@ -82,13 +82,13 @@ const DTF = (() => {
const bytes = new Uint8Array(buffer);
const view = new DataView(buffer);
if (bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF");
if (bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) {
if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF");
if(bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) {
throw new Error("Invalid DTF magic bytes not a DTF file");
}
const version = bytes[3];
if (version !== VERSION) {
if(version !== VERSION) {
throw new Error(`Unsupported DTF version: 0x${version.toString(16).padStart(2, "0")}`);
}
@@ -97,18 +97,18 @@ const DTF = (() => {
const format = bytes[12];
const bpp = BPP[format];
if (bpp === undefined) {
if(bpp === undefined) {
throw new Error(`Unsupported DTF format: 0x${format.toString(16).padStart(2, "0")}`);
}
const expected = HEADER_SIZE + width * height * bpp;
if (bytes.length < expected) {
if(bytes.length < expected) {
throw new Error(`DTF pixel data truncated (expected ${expected} bytes, got ${bytes.length})`);
}
const rgba = new Uint8ClampedArray(width * height * 4);
let src = HEADER_SIZE;
for (let i = 0; i < width * height; i++) {
for(let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
@@ -141,7 +141,7 @@ const DTF = (() => {
const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData);
const out = new Uint8ClampedArray(width * height * 4);
for (let i = 0; i < width * height; i++) {
for(let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA: {
+3 -3
View File
@@ -11,14 +11,14 @@ const DuskPNG = (() => {
// Encode RGBA pixel data into a PNG Buffer via pngjs.
// Returns a Uint8Array (Buffer) if pngjs is available, otherwise null.
function encode(width, height, rgbaData) {
if (!_pngAvailable()) return null;
if(!_pngAvailable()) return null;
const png = new PNG({ width, height });
const src = rgbaData instanceof Uint8ClampedArray
? rgbaData
: new Uint8ClampedArray(rgbaData);
for (let i = 0; i < src.length; i++) {
for(let i = 0; i < src.length; i++) {
png.data[i] = src[i];
}
@@ -29,7 +29,7 @@ const DuskPNG = (() => {
function download(filename, width, height, rgbaData) {
const buf = encode(width, height, rgbaData);
if (buf) {
if(buf) {
// pngjs path
const blob = new Blob([buf], { type: "image/png" });
_triggerDownload(URL.createObjectURL(blob), filename);
+17 -17
View File
@@ -4,7 +4,7 @@
// Returns the smallest power of two >= n.
function nextPow2(n) {
if (n <= 0) return 1;
if(n <= 0) return 1;
let p = 1;
while (p < n) p <<= 1;
return p;
@@ -57,10 +57,10 @@ function computeOutputSize() {
let outW = state.width;
let outH = state.height;
// Apply minimum-size constraints first so pow2 rounding accounts for them.
if (state.minWidth4 && outW < 4) outW = 4;
if (state.minHeight4 && outH < 4) outH = 4;
if (state.padWidthPow2) outW = nextPow2(outW);
if (state.padHeightPow2) outH = nextPow2(outH);
if(state.minWidth4 && outW < 4) outW = 4;
if(state.minHeight4 && outH < 4) outH = 4;
if(state.padWidthPow2) outW = nextPow2(outW);
if(state.padHeightPow2) outH = nextPow2(outH);
return { outW, outH };
}
@@ -69,8 +69,8 @@ function buildPaddedPixels() {
const src = state.pixels;
// Zero-filled Uint8ClampedArray = fully transparent black padding.
const out = new Uint8ClampedArray(outW * outH * 4);
for (let y = 0; y < state.height; y++) {
for (let x = 0; x < state.width; x++) {
for(let y = 0; y < state.height; y++) {
for(let x = 0; x < state.width; x++) {
const si = (y * state.width + x) * 4;
const di = (y * outW + x) * 4;
out[di] = src[si];
@@ -87,8 +87,8 @@ function buildPaddedPixels() {
const CHECKER_CELL = 8;
function drawCheckerboard(w, h) {
for (let y = 0; y < h; y += CHECKER_CELL) {
for (let x = 0; x < w; x += CHECKER_CELL) {
for(let y = 0; y < h; y += CHECKER_CELL) {
for(let x = 0; x < w; x += CHECKER_CELL) {
ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff";
ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y));
}
@@ -96,7 +96,7 @@ function drawCheckerboard(w, h) {
}
function render() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height } = buildPaddedPixels();
const cw = width * state.scale;
@@ -105,7 +105,7 @@ function render() {
canvas.width = cw;
canvas.height = ch;
if (state.bg === "grid") {
if(state.bg === "grid") {
drawCheckerboard(cw, ch);
} else {
ctx.fillStyle = state.bg;
@@ -122,7 +122,7 @@ function render() {
// ─── Info update ─────────────────────────────────────────────────────────────
function updateInfo() {
if (!state.pixels) return;
if(!state.pixels) return;
const { outW, outH } = computeOutputSize();
const bytes = DTF.HEADER_SIZE + outW * outH * DTF.BPP[state.format];
infoInputSize.textContent = `${state.width} × ${state.height}`;
@@ -189,8 +189,8 @@ function applyImageData(width, height, data, filename) {
}
function handleFile(file) {
if (!file) return;
if (file.name.toLowerCase().endsWith(".dtf")) {
if(!file) return;
if(file.name.toLowerCase().endsWith(".dtf")) {
loadDTF(file);
} else {
loadStandardImage(file);
@@ -200,7 +200,7 @@ function handleFile(file) {
// ─── Export ───────────────────────────────────────────────────────────────────
function exportDTF() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height } = buildPaddedPixels();
const buf = DTF.encode(width, height, pixels, state.format);
const blob = new Blob([buf], { type: "application/octet-stream" });
@@ -214,7 +214,7 @@ function exportDTF() {
}
function exportPNG() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height } = buildPaddedPixels();
DuskPNG.download(`${state.filename}.png`, width, height, pixels);
}
@@ -232,7 +232,7 @@ scaleInput.addEventListener("input", () => {
bgSwatches.addEventListener("click", e => {
const btn = e.target.closest(".bg-swatch");
if (!btn) return;
if(!btn) return;
bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
state.bg = btn.dataset.bg;
+17 -17
View File
@@ -43,8 +43,8 @@ const redAsAlphaCheck = document.getElementById("red-as-alpha");
const CHECKER_CELL = 8;
function drawCheckerboard(w, h) {
for (let y = 0; y < h; y += CHECKER_CELL) {
for (let x = 0; x < w; x += CHECKER_CELL) {
for(let y = 0; y < h; y += CHECKER_CELL) {
for(let x = 0; x < w; x += CHECKER_CELL) {
ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff";
ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y));
}
@@ -52,7 +52,7 @@ function drawCheckerboard(w, h) {
}
function render() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height, scale, bg, format } = state;
const cw = width * scale;
@@ -62,7 +62,7 @@ function render() {
canvas.height = ch;
// 1. Background
if (bg === "grid") {
if(bg === "grid") {
drawCheckerboard(cw, ch);
} else {
ctx.fillStyle = bg;
@@ -128,17 +128,17 @@ function loadDTF(file) {
function updateWarnings() {
const warnings = [];
if (state.pixels) {
if(state.pixels) {
const { width, height } = state;
const isPow2 = n => n > 0 && (n & (n - 1)) === 0;
if (width < 4) warnings.push(`Width is below 4 px (${width})`);
if (height < 4) warnings.push(`Height is below 4 px (${height})`);
if (!isPow2(width)) warnings.push(`Width is not a power of two (${width})`);
if (!isPow2(height)) warnings.push(`Height is not a power of two (${height})`);
if(width < 4) warnings.push(`Width is below 4 px (${width})`);
if(height < 4) warnings.push(`Height is below 4 px (${height})`);
if(!isPow2(width)) warnings.push(`Width is not a power of two (${width})`);
if(!isPow2(height)) warnings.push(`Height is not a power of two (${height})`);
const bytes = DTF.HEADER_SIZE + width * height * DTF.BPP[state.format];
if (bytes > 256 * 1024) {
if(bytes > 256 * 1024) {
warnings.push(`Output exceeds 256 KB (${(bytes / 1024).toFixed(1)} KB)`);
}
}
@@ -150,7 +150,7 @@ function updateWarnings() {
}
function updateDtfSize() {
if (!state.pixels) return;
if(!state.pixels) return;
const bytes = DTF.HEADER_SIZE + state.width * state.height * DTF.BPP[state.format];
infoDtfSize.textContent = `${(bytes / 1024).toFixed(1)} KB`;
updateWarnings();
@@ -163,7 +163,7 @@ function applyImageData(width, height, data, filename, formatLabel, format) {
state.filename = filename.replace(/\.[^/.]+$/, "");
// Sync format selector when loading an existing DTF
if (format !== undefined) {
if(format !== undefined) {
state.format = format;
formatSelect.value = format;
redAsAlphaRow.hidden = format !== DTF.FORMAT_ALPHA;
@@ -189,8 +189,8 @@ function applyImageData(width, height, data, filename, formatLabel, format) {
}
function handleFile(file) {
if (!file) return;
if (file.name.toLowerCase().endsWith(".dtf")) {
if(!file) return;
if(file.name.toLowerCase().endsWith(".dtf")) {
loadDTF(file);
} else {
loadStandardImage(file);
@@ -204,7 +204,7 @@ function showError(msg) {
// ─── Export ───────────────────────────────────────────────────────────────────
function exportDTF() {
if (!state.pixels) return;
if(!state.pixels) return;
const buf = DTF.encode(state.width, state.height, state.pixels, state.format, state.redAsAlpha);
const blob = new Blob([buf], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
@@ -217,7 +217,7 @@ function exportDTF() {
}
function exportPNG() {
if (!state.pixels) return;
if(!state.pixels) return;
DuskPNG.download(`${state.filename}.png`, state.width, state.height, state.pixels);
}
@@ -234,7 +234,7 @@ scaleInput.addEventListener("input", () => {
bgSwatches.addEventListener("click", e => {
const btn = e.target.closest(".bg-swatch");
if (!btn) return;
if(!btn) return;
bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
state.bg = btn.dataset.bg;