Mostly nuking old system
This commit is contained in:
@@ -82,7 +82,7 @@ endif()
|
|||||||
add_subdirectory(tools)
|
add_subdirectory(tools)
|
||||||
|
|
||||||
# Assets
|
# Assets
|
||||||
add_subdirectory(assets)
|
# add_subdirectory(assets)
|
||||||
|
|
||||||
# Add libraries
|
# Add libraries
|
||||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
|
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
|
||||||
@@ -176,16 +176,16 @@ if(ENABLE_TESTS)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build assets
|
# Build assets
|
||||||
dusk_run_python(
|
# dusk_run_python(
|
||||||
DUSK_ASSETS_BUILT
|
# DUSK_ASSETS_BUILT
|
||||||
tools.asset.bundle
|
# tools.asset.bundle
|
||||||
--assets ${DUSK_ASSETS_DIR}
|
# --assets ${DUSK_ASSETS_DIR}
|
||||||
--output-assets ${DUSK_BUILT_ASSETS_DIR}
|
# --output-assets ${DUSK_BUILT_ASSETS_DIR}
|
||||||
--output-file ${DUSK_BUILD_DIR}/dusk.dsk
|
# --output-file ${DUSK_BUILD_DIR}/dusk.dsk
|
||||||
--headers-dir ${DUSK_GENERATED_HEADERS_DIR}
|
# --headers-dir ${DUSK_GENERATED_HEADERS_DIR}
|
||||||
--input ${DUSK_ASSETS}
|
# --input ${DUSK_ASSETS}
|
||||||
)
|
# )
|
||||||
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DUSK_ASSETS_BUILT)
|
# add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DUSK_ASSETS_BUILT)
|
||||||
|
|
||||||
# Include generated headers
|
# Include generated headers
|
||||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Copyright (c) 2025 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
set(DUSK_GAME_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}" CACHE INTERNAL "Game assets directory")
|
|
||||||
|
|
||||||
# Palette asset needs to be added before any images.
|
|
||||||
add_subdirectory(palette)
|
|
||||||
|
|
||||||
# Languages need to be added before anything that uses text.
|
|
||||||
add_subdirectory(locale)
|
|
||||||
|
|
||||||
# Rest, order doesn't matter
|
|
||||||
add_asset(SCRIPT init.lua)
|
|
||||||
|
|
||||||
# Subdirs
|
|
||||||
# add_subdirectory(entity)
|
|
||||||
# add_subdirectory(map)
|
|
||||||
add_subdirectory(ui)
|
|
||||||
add_subdirectory(minesweeper)
|
|
||||||
add_subdirectory(scene)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Copyright (c) 2025 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
add_asset(TILESET entities.tsx)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 336 B |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<tileset version="1.10" tiledversion="1.11.2" name="entities" tilewidth="16" tileheight="16" tilecount="64" columns="8">
|
|
||||||
<image source="entities.png" width="128" height="128"/>
|
|
||||||
</tileset>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Copyright (c) 2025 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
add_asset(LANGUAGE en_US.po)
|
|
||||||
@@ -7,9 +7,3 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "ui.test"
|
msgid "ui.test"
|
||||||
msgstr "Hello this is a test."
|
msgstr "Hello this is a test."
|
||||||
|
|
||||||
msgid "map.test"
|
|
||||||
msgstr "This is a map test."
|
|
||||||
|
|
||||||
msgid "test.test2"
|
|
||||||
msgstr "This is another test."
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# Copyright (c) 2025 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
add_asset(PALETTE palette0.png)
|
|
||||||
add_asset(PALETTE paletteMinesweeper.png)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 241 B |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 168 B |
@@ -1,7 +0,0 @@
|
|||||||
# Copyright (c) 2025 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
add_asset(SCRIPT initial.lua)
|
|
||||||
add_asset(SCRIPT minesweeper.lua)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
module('spritebatch')
|
|
||||||
module('camera')
|
|
||||||
module('color')
|
|
||||||
module('text')
|
|
||||||
module('screen')
|
|
||||||
module('time')
|
|
||||||
module('glm')
|
|
||||||
|
|
||||||
screenSetBackground(colorBlack())
|
|
||||||
camera = cameraCreate(CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC)
|
|
||||||
|
|
||||||
text = "Hello World"
|
|
||||||
|
|
||||||
function sceneDispose()
|
|
||||||
end
|
|
||||||
|
|
||||||
function sceneUpdate()
|
|
||||||
end
|
|
||||||
|
|
||||||
function sceneRender()
|
|
||||||
-- UI Test
|
|
||||||
cameraPushMatrix(camera)
|
|
||||||
camera.bottom = screenGetHeight()
|
|
||||||
camera.right = screenGetWidth()
|
|
||||||
|
|
||||||
width, height = textMeasure(text)
|
|
||||||
x = (screenGetWidth() - width)
|
|
||||||
x = math.sin(TIME.time * 2) * (x / 2) + (x / 2)
|
|
||||||
y = (screenGetHeight() - height) / 2
|
|
||||||
y = math.cos(TIME.time * 3) * (y) + (y)
|
|
||||||
|
|
||||||
-- For each letter
|
|
||||||
for i = 1, #text do
|
|
||||||
letter = text:sub(i, i)
|
|
||||||
letterWidth, _ = textMeasure(letter)
|
|
||||||
|
|
||||||
-- Draw letter with rainbow color
|
|
||||||
textDraw(x, y, letter, colorRainbow((i - 1) * 0.1, 8))
|
|
||||||
x = x + letterWidth
|
|
||||||
end
|
|
||||||
|
|
||||||
cameraPopMatrix()
|
|
||||||
end
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 758 B |
Binary file not shown.
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<tileset version="1.10" tiledversion="1.11.2" name="prarie" tilewidth="16" tileheight="16" tilecount="21" columns="7">
|
|
||||||
<image source="prarie.png" width="112" height="48"/>
|
|
||||||
</tileset>
|
|
||||||
@@ -232,6 +232,20 @@ errorret_t assetLoad(const char_t *filename, void *output) {
|
|||||||
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
|
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
|
||||||
assertNotNull(output, "Output pointer cannot be NULL.");
|
assertNotNull(output, "Output pointer cannot be NULL.");
|
||||||
|
|
||||||
|
// Determine the asset type by reading the extension
|
||||||
|
const assettypedef_t *def = NULL;
|
||||||
|
for(uint_fast8_t i = 0; i < ASSET_TYPE_COUNT; i++) {
|
||||||
|
const assettypedef_t *cmp = &ASSET_TYPE_DEFINITIONS[i];
|
||||||
|
assertNotNull(cmp, "Asset type definition cannot be NULL.");
|
||||||
|
assertNotNull(cmp->extension, "Asset type definition has NULL extension.");
|
||||||
|
if(!stringEndsWithCaseInsensitive(filename, cmp->extension)) continue;
|
||||||
|
def = cmp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(def == NULL) {
|
||||||
|
errorThrow("Unknown asset type for file: %s", filename);
|
||||||
|
}
|
||||||
|
|
||||||
// Get file size of the asset.
|
// Get file size of the asset.
|
||||||
zip_stat_t st;
|
zip_stat_t st;
|
||||||
zip_stat_init(&st);
|
zip_stat_init(&st);
|
||||||
@@ -241,8 +255,8 @@ errorret_t assetLoad(const char_t *filename, void *output) {
|
|||||||
|
|
||||||
// Minimum file size.
|
// Minimum file size.
|
||||||
zip_int64_t fileSize = (zip_int64_t)st.size;
|
zip_int64_t fileSize = (zip_int64_t)st.size;
|
||||||
if(fileSize < sizeof(assetheader_t)) {
|
if(fileSize <= 0) {
|
||||||
errorThrow("Asset file too small to contain header: %s", filename);
|
errorThrow("Asset file is empty: %s", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to open the file
|
// Try to open the file
|
||||||
@@ -250,61 +264,19 @@ errorret_t assetLoad(const char_t *filename, void *output) {
|
|||||||
if(file == NULL) {
|
if(file == NULL) {
|
||||||
errorThrow("Failed to open asset file: %s", filename);
|
errorThrow("Failed to open asset file: %s", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the header.
|
|
||||||
zip_int64_t bytesRemaining = fileSize;
|
|
||||||
assetheader_t header;
|
|
||||||
memoryZero(&header, sizeof(assetheader_t));
|
|
||||||
zip_int64_t bytesRead = zip_fread(
|
|
||||||
file,
|
|
||||||
&header,
|
|
||||||
(zip_uint64_t)sizeof(assetheader_t)
|
|
||||||
);
|
|
||||||
if((size_t)bytesRead != sizeof(assetheader_t)) {
|
|
||||||
zip_fclose(file);
|
|
||||||
errorThrow("Failed to read asset header for: %s", filename);
|
|
||||||
}
|
|
||||||
bytesRemaining -= (zip_uint64_t)bytesRead;
|
|
||||||
|
|
||||||
assertTrue(sizeof(assetheader_t) == ASSET_HEADER_SIZE, "Asset header size mismatch.");
|
// Load the asset data
|
||||||
assertTrue(bytesRead == ASSET_HEADER_SIZE, "Asset header read size mismatch.");
|
|
||||||
|
|
||||||
// Find the asset type based on the header
|
|
||||||
const assettypedef_t *def = NULL;
|
|
||||||
for(uint_fast8_t i = 0; i < ASSET_TYPE_COUNT; i++) {
|
|
||||||
const assettypedef_t *cmp = &ASSET_TYPE_DEFINITIONS[i];
|
|
||||||
if(cmp->header == NULL) continue;
|
|
||||||
|
|
||||||
// strcmp didn't work because it's a fixed char_t[3] I think, or maybe
|
|
||||||
// because of the packed struct?
|
|
||||||
bool_t match = true;
|
|
||||||
for(size_t h = 0; h < ASSET_HEADER_SIZE; h++) {
|
|
||||||
if(header.header[h] == cmp->header[h]) continue;
|
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if(!match) continue;
|
|
||||||
|
|
||||||
def = cmp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if(def == NULL) {
|
|
||||||
zip_fclose(file);
|
|
||||||
errorThrow("Unknown asset type for file: %s", filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We found the asset type, now load the asset data
|
|
||||||
switch(def->loadStrategy) {
|
switch(def->loadStrategy) {
|
||||||
case ASSET_LOAD_STRAT_ENTIRE:
|
case ASSET_LOAD_STRAT_ENTIRE:
|
||||||
assertNotNull(def->entire, "Asset load function cannot be NULL.");
|
assertNotNull(def->entire, "Asset load function cannot be NULL.");
|
||||||
|
|
||||||
// Must have more to read
|
// Must have more to read
|
||||||
if(bytesRemaining <= 0) {
|
if(fileSize <= 0) {
|
||||||
zip_fclose(file);
|
zip_fclose(file);
|
||||||
errorThrow("No data remaining to read for asset: %s", filename);
|
errorThrow("No data remaining to read for asset: %s", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bytesRemaining > def->dataSize) {
|
if(fileSize > def->dataSize) {
|
||||||
zip_fclose(file);
|
zip_fclose(file);
|
||||||
errorThrow(
|
errorThrow(
|
||||||
"Asset file has too much data remaining after header: %s",
|
"Asset file has too much data remaining after header: %s",
|
||||||
@@ -313,20 +285,20 @@ errorret_t assetLoad(const char_t *filename, void *output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create space to read the entire asset data
|
// Create space to read the entire asset data
|
||||||
void *data = memoryAllocate(bytesRemaining);
|
void *data = memoryAllocate(fileSize);
|
||||||
if(!data) {
|
if(!data) {
|
||||||
zip_fclose(file);
|
zip_fclose(file);
|
||||||
errorThrow("Failed to allocate memory for asset data of file: %s", filename);
|
errorThrow("Failed to allocate memory for asset data of file: %s", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in the asset data.
|
// Read in the asset data.
|
||||||
bytesRead = zip_fread(file, data, bytesRemaining);
|
zip_int64_t bytesRead = zip_fread(file, data, fileSize);
|
||||||
if(bytesRead == 0 || bytesRead > bytesRemaining) {
|
if(bytesRead == 0 || bytesRead > fileSize) {
|
||||||
memoryFree(data);
|
memoryFree(data);
|
||||||
zip_fclose(file);
|
zip_fclose(file);
|
||||||
errorThrow("Failed to read asset data for file: %s", filename);
|
errorThrow("Failed to read asset data for file: %s", filename);
|
||||||
}
|
}
|
||||||
bytesRemaining -= bytesRead;
|
fileSize -= bytesRead;
|
||||||
|
|
||||||
// Close the file now we have the data
|
// Close the file now we have the data
|
||||||
zip_fclose(file);
|
zip_fclose(file);
|
||||||
|
|||||||
@@ -72,12 +72,6 @@ static const char_t *ASSET_SEARCH_PATHS[] = {
|
|||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
typedef struct {
|
|
||||||
char_t header[ASSET_HEADER_SIZE];
|
|
||||||
} assetheader_t;
|
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
zip_t *zip;
|
zip_t *zip;
|
||||||
char_t systemPath[FILENAME_MAX];
|
char_t systemPath[FILENAME_MAX];
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "type/assetpaletteimage.h"
|
#include "type/assettexture.h"
|
||||||
#include "type/assetalphaimage.h"
|
|
||||||
#include "type/assetlanguage.h"
|
#include "type/assetlanguage.h"
|
||||||
#include "type/assetscript.h"
|
#include "type/assetscript.h"
|
||||||
#include "type/assetmap.h"
|
#include "type/assetmap.h"
|
||||||
@@ -17,8 +16,7 @@
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_TYPE_NULL,
|
ASSET_TYPE_NULL,
|
||||||
|
|
||||||
ASSET_TYPE_PALETTE_IMAGE,
|
ASSET_TYPE_TEXTURE,
|
||||||
ASSET_TYPE_ALPHA_IMAGE,
|
|
||||||
ASSET_TYPE_LANGUAGE,
|
ASSET_TYPE_LANGUAGE,
|
||||||
ASSET_TYPE_SCRIPT,
|
ASSET_TYPE_SCRIPT,
|
||||||
ASSET_TYPE_MAP,
|
ASSET_TYPE_MAP,
|
||||||
@@ -38,7 +36,7 @@ typedef struct assetcustom_s {
|
|||||||
} assetcustom_t;
|
} assetcustom_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char_t *header;
|
const char_t *extension;
|
||||||
const size_t dataSize;
|
const size_t dataSize;
|
||||||
const assetloadstrat_t loadStrategy;
|
const assetloadstrat_t loadStrategy;
|
||||||
union {
|
union {
|
||||||
@@ -52,41 +50,34 @@ static const assettypedef_t ASSET_TYPE_DEFINITIONS[ASSET_TYPE_COUNT] = {
|
|||||||
0
|
0
|
||||||
},
|
},
|
||||||
|
|
||||||
[ASSET_TYPE_PALETTE_IMAGE] = {
|
[ASSET_TYPE_TEXTURE] = {
|
||||||
.header = "DPI",
|
.extension = "dpt",
|
||||||
.loadStrategy = ASSET_LOAD_STRAT_ENTIRE,
|
.loadStrategy = ASSET_LOAD_STRAT_ENTIRE,
|
||||||
.dataSize = sizeof(assetpaletteimage_t),
|
.dataSize = sizeof(assettexture_t),
|
||||||
.entire = assetPaletteImageLoad
|
.entire = assetTextureLoad
|
||||||
},
|
|
||||||
|
|
||||||
[ASSET_TYPE_ALPHA_IMAGE] = {
|
|
||||||
.header = "DAI",
|
|
||||||
.loadStrategy = ASSET_LOAD_STRAT_ENTIRE,
|
|
||||||
.dataSize = sizeof(assetalphaimage_t),
|
|
||||||
.entire = assetAlphaImageLoad
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[ASSET_TYPE_LANGUAGE] = {
|
[ASSET_TYPE_LANGUAGE] = {
|
||||||
.header = "DLF",
|
.extension = "DLF",
|
||||||
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
||||||
.custom = assetLanguageHandler
|
.custom = assetLanguageHandler
|
||||||
},
|
},
|
||||||
|
|
||||||
[ASSET_TYPE_SCRIPT] = {
|
[ASSET_TYPE_SCRIPT] = {
|
||||||
.header = "DSF",
|
.extension = "lua",
|
||||||
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
||||||
.custom = assetScriptHandler
|
.custom = assetScriptHandler
|
||||||
},
|
},
|
||||||
|
|
||||||
[ASSET_TYPE_MAP] = {
|
// [ASSET_TYPE_MAP] = {
|
||||||
.header = "DMF",
|
// .extension = "DMF",
|
||||||
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
// .loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
||||||
.custom = assetMapHandler
|
// .custom = assetMapHandler
|
||||||
},
|
// },
|
||||||
|
|
||||||
[ASSET_TYPE_MAP_CHUNK] = {
|
// [ASSET_TYPE_MAP_CHUNK] = {
|
||||||
.header = "DMC",
|
// .extension = "DMC",
|
||||||
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
// .loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
|
||||||
.custom = assetMapChunkHandler
|
// .custom = assetMapChunkHandler
|
||||||
},
|
// },
|
||||||
};
|
};
|
||||||
@@ -6,8 +6,7 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
assetalphaimage.c
|
assettexture.c
|
||||||
assetpaletteimage.c
|
|
||||||
assetlanguage.c
|
assetlanguage.c
|
||||||
assetscript.c
|
assetscript.c
|
||||||
assetmap.c
|
assetmap.c
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "assetalphaimage.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "display/texture.h"
|
|
||||||
|
|
||||||
#include "debug/debug.h"
|
|
||||||
|
|
||||||
errorret_t assetAlphaImageLoad(void *data, void *output) {
|
|
||||||
assertNotNull(data, "Data pointer cannot be NULL.");
|
|
||||||
assertNotNull(output, "Output pointer cannot be NULL.");
|
|
||||||
|
|
||||||
assetalphaimage_t *dataPtr = (assetalphaimage_t *)data;
|
|
||||||
texture_t *outputPtr = (texture_t *)output;
|
|
||||||
|
|
||||||
// Fix endian
|
|
||||||
dataPtr->width = le32toh(dataPtr->width);
|
|
||||||
dataPtr->height = le32toh(dataPtr->height);
|
|
||||||
|
|
||||||
textureInit(
|
|
||||||
outputPtr,
|
|
||||||
dataPtr->width,
|
|
||||||
dataPtr->height,
|
|
||||||
TEXTURE_FORMAT_ALPHA,
|
|
||||||
(texturedata_t){ .alpha = { .data = dataPtr->pixels } }
|
|
||||||
);
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
#define ASSET_ALPHA_IMAGE_WIDTH_MAX 256
|
|
||||||
#define ASSET_ALPHA_IMAGE_HEIGHT_MAX 256
|
|
||||||
#define ASSET_ALPHA_IMAGE_SIZE_MAX ( \
|
|
||||||
ASSET_ALPHA_IMAGE_WIDTH_MAX * ASSET_ALPHA_IMAGE_HEIGHT_MAX \
|
|
||||||
)
|
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
typedef struct {
|
|
||||||
uint32_t width;
|
|
||||||
uint32_t height;
|
|
||||||
uint8_t pixels[ASSET_ALPHA_IMAGE_SIZE_MAX];
|
|
||||||
} assetalphaimage_t;
|
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an alpha image asset from the given asset structure. The asset must
|
|
||||||
* be of type ASSET_TYPE_ALPHA_IMAGE and must be loaded.
|
|
||||||
*
|
|
||||||
* @param asset The asset to load the alpha image from.
|
|
||||||
* @return An error code.
|
|
||||||
*/
|
|
||||||
errorret_t assetAlphaImageLoad(void *data, void *output);
|
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "locale/language/keys.h"
|
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "duskdefs.h"
|
#include "duskdefs.h"
|
||||||
#include <zip.h>
|
#include <zip.h>
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
#define ASSET_PALETTE_IMAGE_WIDTH_MAX 256
|
|
||||||
#define ASSET_PALETTE_IMAGE_HEIGHT_MAX 256
|
|
||||||
#define ASSET_PALETTE_IMAGE_SIZE_MAX ( \
|
|
||||||
ASSET_PALETTE_IMAGE_WIDTH_MAX * ASSET_PALETTE_IMAGE_HEIGHT_MAX \
|
|
||||||
)
|
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
typedef struct {
|
|
||||||
uint32_t width;
|
|
||||||
uint32_t height;
|
|
||||||
uint8_t paletteIndex;
|
|
||||||
uint8_t palette[ASSET_PALETTE_IMAGE_SIZE_MAX];
|
|
||||||
} assetpaletteimage_t;
|
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a palette image asset from the given data pointer into the output
|
|
||||||
* texture.
|
|
||||||
*
|
|
||||||
* @param data Pointer to the raw assetpaletteimage_t data.
|
|
||||||
* @param output Pointer to the texture_t to load the image into.
|
|
||||||
* @return An error code.
|
|
||||||
*/
|
|
||||||
errorret_t assetPaletteImageLoad(void *data, void *output);
|
|
||||||
@@ -5,37 +5,38 @@
|
|||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "assetpaletteimage.h"
|
#include "assettexture.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "display/texture.h"
|
#include "display/texture.h"
|
||||||
#include "display/palette/palettelist.h"
|
|
||||||
|
|
||||||
errorret_t assetPaletteImageLoad(void *data, void *output) {
|
errorret_t assetPaletteImageLoad(void *data, void *output) {
|
||||||
assertNotNull(data, "Data pointer cannot be NULL.");
|
assertNotNull(data, "Data pointer cannot be NULL.");
|
||||||
assertNotNull(output, "Output pointer cannot be NULL.");
|
assertNotNull(output, "Output pointer cannot be NULL.");
|
||||||
|
|
||||||
assetpaletteimage_t *assetData = (assetpaletteimage_t *)data;
|
assettexture_t *assetData = (assettexture_t *)data;
|
||||||
texture_t *texture = (texture_t *)output;
|
texture_t *texture = (texture_t *)output;
|
||||||
|
|
||||||
// Fix endian
|
// Fix endian
|
||||||
assetData->width = le32toh(assetData->width);
|
assetData->width = le32toh(assetData->width);
|
||||||
assetData->height = le32toh(assetData->height);
|
assetData->height = le32toh(assetData->height);
|
||||||
|
|
||||||
const palette_t *pal = PALETTE_LIST[assetData->paletteIndex];
|
|
||||||
assertNotNull(pal, "Palette index is out of bounds");
|
|
||||||
|
|
||||||
textureInit(
|
errorThrow("Hello World\n");
|
||||||
texture,
|
|
||||||
assetData->width,
|
|
||||||
assetData->height,
|
|
||||||
TEXTURE_FORMAT_PALETTE,
|
|
||||||
(texturedata_t){
|
|
||||||
.palette = {
|
|
||||||
.palette = pal,
|
|
||||||
.data = assetData->palette
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
errorOk();
|
// const palette_t *pal = PALETTE_LIST[assetData->paletteIndex];
|
||||||
|
// assertNotNull(pal, "Palette index is out of bounds");
|
||||||
|
|
||||||
|
// textureInit(
|
||||||
|
// texture,
|
||||||
|
// assetData->width,
|
||||||
|
// assetData->height,
|
||||||
|
// TEXTURE_FORMAT_PALETTE,
|
||||||
|
// (texturedata_t){
|
||||||
|
// .palette = {
|
||||||
|
// .palette = pal,
|
||||||
|
// .data = assetData->palette
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// errorOk();
|
||||||
}
|
}
|
||||||
34
src/asset/type/assettexture.h
Normal file
34
src/asset/type/assettexture.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define ASSET_TEXTURE_WIDTH_MAX 256
|
||||||
|
#define ASSET_TEXTURE_HEIGHT_MAX 256
|
||||||
|
#define ASSET_TEXTURE_SIZE_MAX ( \
|
||||||
|
ASSET_TEXTURE_WIDTH_MAX * ASSET_TEXTURE_HEIGHT_MAX \
|
||||||
|
)
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint8_t paletteIndex;
|
||||||
|
uint8_t palette[ASSET_TEXTURE_SIZE_MAX];
|
||||||
|
} assettexture_t;
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a palettized texture from the given data pointer into the output
|
||||||
|
* texture.
|
||||||
|
*
|
||||||
|
* @param data Pointer to the raw assettexture_t data.
|
||||||
|
* @param output Pointer to the texture_t to load the image into.
|
||||||
|
* @return An error code.
|
||||||
|
*/
|
||||||
|
errorret_t assetTextureLoad(void *data, void *output);
|
||||||
@@ -6,4 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
palette.c
|
||||||
)
|
)
|
||||||
8
src/display/palette/palette.c
Normal file
8
src/display/palette/palette.c
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "palette.h"
|
||||||
@@ -8,7 +8,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "display/color.h"
|
#include "display/color.h"
|
||||||
|
|
||||||
|
#define PALETTE_COUNT 4
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const uint8_t colorCount;
|
const uint8_t colorCount;
|
||||||
const color_t *colors;
|
const color_t *colors;
|
||||||
} palette_t;
|
} palette_t;
|
||||||
|
|
||||||
|
extern palette_t PALETTES[PALETTE_COUNT];
|
||||||
@@ -12,14 +12,15 @@
|
|||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
|
||||||
texture_t DEFAULT_FONT_TEXTURE;
|
texture_t DEFAULT_FONT_TEXTURE;
|
||||||
|
tileset_t DEFAULT_FONT_TILESET;
|
||||||
|
|
||||||
errorret_t textInit(void) {
|
errorret_t textInit(void) {
|
||||||
errorChain(assetLoad(DEFAULT_FONT_TILESET.image, &DEFAULT_FONT_TEXTURE));
|
// errorChain(assetLoad(DEFAULT_FONT_TILESET.image, &DEFAULT_FONT_TEXTURE));
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void textDispose(void) {
|
void textDispose(void) {
|
||||||
textureDispose(&DEFAULT_FONT_TEXTURE);
|
// textureDispose(&DEFAULT_FONT_TEXTURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void textDrawChar(
|
void textDrawChar(
|
||||||
|
|||||||
@@ -9,12 +9,11 @@
|
|||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "display/texture.h"
|
#include "display/texture.h"
|
||||||
#include "display/tileset/tileset.h"
|
#include "display/tileset/tileset.h"
|
||||||
#include "display/tileset/tileset_minogram.h"
|
|
||||||
|
|
||||||
#define TEXT_CHAR_START '!'
|
#define TEXT_CHAR_START '!'
|
||||||
|
|
||||||
extern texture_t DEFAULT_FONT_TEXTURE;
|
// extern texture_t DEFAULT_FONT_TEXTURE;
|
||||||
#define DEFAULT_FONT_TILESET TILESET_MINOGRAM
|
// #define DEFAULT_FONT_TILESET TILESET_MINOGRAM
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the text system.
|
* Initializes the text system.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "display/palette/palettelist.h"
|
|
||||||
#include "debug/debug.h"
|
#include "debug/debug.h"
|
||||||
|
|
||||||
const texture_t *TEXTURE_BOUND = NULL;
|
const texture_t *TEXTURE_BOUND = NULL;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "tileset.h"
|
#include "tileset.h"
|
||||||
#include "display/tileset/tilesetlist.h"
|
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
@@ -34,13 +33,4 @@ void tilesetPositionGetUV(
|
|||||||
outUV[1] = ((float_t)row) * tileset->uv[1];
|
outUV[1] = ((float_t)row) * tileset->uv[1];
|
||||||
outUV[2] = outUV[0] + tileset->uv[0];
|
outUV[2] = outUV[0] + tileset->uv[0];
|
||||||
outUV[3] = outUV[1] + tileset->uv[1];
|
outUV[3] = outUV[1] + tileset->uv[1];
|
||||||
}
|
|
||||||
|
|
||||||
const tileset_t * tilesetGetByName(const char_t *name) {
|
|
||||||
assertStrLenMin(name, 1, "Tileset name cannot be empty");
|
|
||||||
for(uint32_t i = 0; i < TILESET_LIST_COUNT; i++) {
|
|
||||||
if(stringCompare(TILESET_LIST[i]->name, name) != 0) continue;
|
|
||||||
return TILESET_LIST[i];
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,4 @@ void tilesetPositionGetUV(
|
|||||||
const uint16_t column,
|
const uint16_t column,
|
||||||
const uint16_t row,
|
const uint16_t row,
|
||||||
vec4 outUV
|
vec4 outUV
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a tileset by its name.
|
|
||||||
*
|
|
||||||
* @param name The name of the tileset to get.
|
|
||||||
* @return The tileset with the given name, or NULL if not found.
|
|
||||||
*/
|
|
||||||
const tileset_t* tilesetGetByName(const char_t *name);
|
|
||||||
13
src/locale/locale.h
Normal file
13
src/locale/locale.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *nothing;
|
||||||
|
} dusklocale_t;
|
||||||
@@ -4,14 +4,4 @@
|
|||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
add_subdirectory(run_python)
|
add_subdirectory(run_python)
|
||||||
add_subdirectory(env_to_h)
|
add_subdirectory(env_to_h)
|
||||||
|
|
||||||
# Function that adds an asset to be compiled
|
|
||||||
function(add_asset ASSET_TYPE ASSET_PATH)
|
|
||||||
set(FULL_ASSET_PATH "${CMAKE_CURRENT_LIST_DIR}/${ASSET_PATH}")
|
|
||||||
string(JOIN "%" ASSETS_ARGS ${ARGN})
|
|
||||||
list(APPEND DUSK_ASSETS
|
|
||||||
"${ASSET_TYPE}#${FULL_ASSET_PATH}#${ASSETS_ARGS}"
|
|
||||||
)
|
|
||||||
set(DUSK_ASSETS ${DUSK_ASSETS} CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
|
||||||
endfunction()
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
from tools.util.type import detectType, stringToCType, typeToCType
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Color CSV to .h defines")
|
parser = argparse.ArgumentParser(description="Color CSV to .h defines")
|
||||||
parser.add_argument("--csv", required=True, help="Path to color CSV file")
|
parser.add_argument("--csv", required=True, help="Path to color CSV file")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
from tools.util.type import detectType, stringToCType, typeToCType
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Input CSV to .h defines")
|
parser = argparse.ArgumentParser(description="Input CSV to .h defines")
|
||||||
parser.add_argument("--csv", required=True, help="Path to Input CSV file")
|
parser.add_argument("--csv", required=True, help="Path to Input CSV file")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
from tools.util.type import detectType, stringToCType, typeToCType
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Item CSV to .h defines")
|
parser = argparse.ArgumentParser(description="Item CSV to .h defines")
|
||||||
parser.add_argument("--csv", required=True, help="Path to item CSV file")
|
parser.add_argument("--csv", required=True, help="Path to item CSV file")
|
||||||
|
|||||||
698
tools/palette-indexer.html
Normal file
698
tools/palette-indexer.html
Normal file
@@ -0,0 +1,698 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dusk Tools / Palette Indexer</title>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Dusk Palette Indexer</h1>
|
||||||
|
<p>
|
||||||
|
This tool takes an input image and creates a palettized version of it.
|
||||||
|
You will get two files, a .dpf (Dusk Palette File) and a .dpt
|
||||||
|
(Dusk Palettized Texture). The first being the palette, which I recommend
|
||||||
|
reusing across multiple images, and the second being the indexed image,
|
||||||
|
which uses the colors from the palette.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Also, dusk previously supported Alpha textures, but due to so many little
|
||||||
|
platform differences I realized it is simpler and about as much work to
|
||||||
|
get palettized textures working instead. As a result I suggest creating a
|
||||||
|
single palette for your alpha textures, and reusing that globally.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Input Image</h2>
|
||||||
|
<div>
|
||||||
|
<input type="file" data-file-input />
|
||||||
|
</div>
|
||||||
|
<p data-file-error style="color:red;display:none;"></p>
|
||||||
|
<canvas data-input-preview style="border:1px solid black;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Palette</h2>
|
||||||
|
<div>
|
||||||
|
<button data-palette-add>Add Color</button>
|
||||||
|
<button data-palette-append>Append Palette</button>
|
||||||
|
<button data-palette-clear>Clear Palette</button>
|
||||||
|
<button data-palette-optimize>Optimize Palette</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-palette-entries style="display:grid;grid-template-columns:repeat(auto-fill, minmax(400px, 1fr));gap:16px;margin-top:16px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Preview Background:
|
||||||
|
<button data-page-bg-white>White</button>
|
||||||
|
<button data-page-bg-transparent>Black</button>
|
||||||
|
<button data-page-bg-checkerboard>Checkerboard</button>
|
||||||
|
<button data-page-bg-magenta>Magenta</button>
|
||||||
|
<button data-page-bg-blue>Blue</button>
|
||||||
|
<button data-page-bg-green>Green</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Palette Preview</h2>
|
||||||
|
|
||||||
|
<h3>Palette</h3>
|
||||||
|
<div>
|
||||||
|
<button data-palette-download>Download Palette</button>
|
||||||
|
</div>
|
||||||
|
<canvas data-palette-preview style="border:1px solid black;"></canvas>
|
||||||
|
<p data-palette-information></p>
|
||||||
|
|
||||||
|
<h3>Indexed Image</h3>
|
||||||
|
<div>
|
||||||
|
<button data-indexed-download>Download Indexed Image</button>
|
||||||
|
</div>
|
||||||
|
<div data-output-error style="color:red;display:none;"></div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Preview Scale:
|
||||||
|
<input type="number" value="2" data-indexed-preview-scale min="1" step="1" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<canvas data-output-preview style="border:1px solid black;"></canvas>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const elError = document.querySelector('[data-file-error]');
|
||||||
|
const elFile = document.querySelector('[data-file-input]');
|
||||||
|
const elInputPreview = document.querySelector('[data-input-preview]');
|
||||||
|
const elPalettePreview = document.querySelector('[data-palette-preview]');
|
||||||
|
const elOutputPreview = document.querySelector('[data-output-preview]');
|
||||||
|
const elPaletteInformation = document.querySelector('[data-palette-information]');
|
||||||
|
const elSourceFromInput = document.querySelector('[data-source-from-input]');
|
||||||
|
const elPaletteAdd = document.querySelector('[data-palette-add]');
|
||||||
|
const elPaletteEntries = document.querySelector('[data-palette-entries]');
|
||||||
|
const elPreviewScale = document.querySelector('[data-indexed-preview-scale]');
|
||||||
|
const elClearPalette = document.querySelector('[data-palette-clear]');
|
||||||
|
const elAppendPalette = document.querySelector('[data-palette-append]');
|
||||||
|
const elOutputError = document.querySelector('[data-output-error]');
|
||||||
|
const elOptimizePalette = document.querySelector('[data-palette-optimize]');
|
||||||
|
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
|
||||||
|
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
|
||||||
|
const btnBackgroundCheckerboard = document.querySelector('[data-page-bg-checkerboard]');
|
||||||
|
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
|
||||||
|
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
|
||||||
|
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
|
||||||
|
const btnDownloadPalette = document.querySelector('[data-palette-download]');
|
||||||
|
const btnDownloadImage = document.querySelector('[data-indexed-download]');
|
||||||
|
|
||||||
|
let imageWidth = 0;
|
||||||
|
let imageHeight = 0;
|
||||||
|
let palette = [];// Array of [ r, g, b, a ]
|
||||||
|
let indexedImage = [ ];// Array of indexes
|
||||||
|
|
||||||
|
const nextPowerOfTwo = (x) => {
|
||||||
|
return Math.pow(2, Math.ceil(Math.log2(x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourcePaletteFromImage = (image) => {
|
||||||
|
// Get unique colors
|
||||||
|
const elCanvas = document.createElement('canvas');
|
||||||
|
elCanvas.width = image.width;
|
||||||
|
elCanvas.height = image.height;
|
||||||
|
const ctx = elCanvas.getContext('2d');
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
const palette = [];
|
||||||
|
for(let i = 0; i < data.length; i += 4) {
|
||||||
|
const color = [ data[i], data[i + 1], data[i + 2], data[i + 3] ];
|
||||||
|
if(!palette.some(c => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3])) {
|
||||||
|
palette.push(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOutput = () => {
|
||||||
|
elOutputError.style.display = 'none';
|
||||||
|
|
||||||
|
// Update palette preview
|
||||||
|
const paletteScale = 8;
|
||||||
|
const uniquePalette = palette.filter((color, index) => {
|
||||||
|
if(color[3] === 0) {
|
||||||
|
// Find any other fully transparent pixel.
|
||||||
|
return index === palette.findIndex(c => c[3] === 0);
|
||||||
|
}
|
||||||
|
return index === palette.findIndex(c => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3]);
|
||||||
|
});
|
||||||
|
elPalettePreview.width = uniquePalette.length * paletteScale;
|
||||||
|
elPalettePreview.height = paletteScale;
|
||||||
|
const paletteCtx = elPalettePreview.getContext('2d');
|
||||||
|
uniquePalette.forEach((color, index) => {
|
||||||
|
paletteCtx.fillStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`;
|
||||||
|
paletteCtx.fillRect(index * paletteScale, 0, paletteScale, paletteScale);
|
||||||
|
});
|
||||||
|
elPaletteInformation.textContent = `Palette contains ${uniquePalette.length} colors (${palette.length - uniquePalette.length} duplicates).`;
|
||||||
|
|
||||||
|
// Update palette entries
|
||||||
|
elPaletteEntries.innerHTML = '';
|
||||||
|
palette.forEach((color, index) => {
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
|
||||||
|
const epar = document.createElement('span');
|
||||||
|
epar.textContent = `Index ${index}`;
|
||||||
|
|
||||||
|
const ecolor = document.createElement('span');
|
||||||
|
ecolor.style.display = 'inline-block';
|
||||||
|
ecolor.style.width = '16px';
|
||||||
|
ecolor.style.height = '16px';
|
||||||
|
ecolor.style.marginLeft = '8px';
|
||||||
|
ecolor.style.backgroundColor = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`;
|
||||||
|
ecolor.style.border = '1px solid black';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.appendChild(epar);
|
||||||
|
header.appendChild(ecolor);
|
||||||
|
|
||||||
|
const inputR = document.createElement('input');
|
||||||
|
inputR.type = 'number';
|
||||||
|
inputR.value = color[0];
|
||||||
|
inputR.min = 0;
|
||||||
|
inputR.max = 255;
|
||||||
|
inputR.setAttribute('data-palette-index', index);
|
||||||
|
inputR.setAttribute('data-palette-channel', '0');
|
||||||
|
inputR.addEventListener('change', (event) => {
|
||||||
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
||||||
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
||||||
|
const value = parseInt(event.target.value);
|
||||||
|
if(isNaN(value) || value < 0 || value > 255) {
|
||||||
|
event.target.value = palette[idx][channel];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
palette[idx][channel] = value;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputG = document.createElement('input');
|
||||||
|
inputG.type = 'number';
|
||||||
|
inputG.value = color[1];
|
||||||
|
inputG.min = 0;
|
||||||
|
inputG.max = 255;
|
||||||
|
inputG.setAttribute('data-palette-index', index);
|
||||||
|
inputG.setAttribute('data-palette-channel', '1');
|
||||||
|
inputG.addEventListener('change', (event) => {
|
||||||
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
||||||
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
||||||
|
const value = parseInt(event.target.value);
|
||||||
|
if(isNaN(value) || value < 0 || value > 255) {
|
||||||
|
event.target.value = palette[idx][channel];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
palette[idx][channel] = value;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputB = document.createElement('input');
|
||||||
|
inputB.type = 'number';
|
||||||
|
inputB.value = color[2];
|
||||||
|
inputB.min = 0;
|
||||||
|
inputB.max = 255;
|
||||||
|
inputB.setAttribute('data-palette-index', index);
|
||||||
|
inputB.setAttribute('data-palette-channel', '2');
|
||||||
|
inputB.addEventListener('change', (event) => {
|
||||||
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
||||||
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
||||||
|
const value = parseInt(event.target.value);
|
||||||
|
if(isNaN(value) || value < 0 || value > 255) {
|
||||||
|
event.target.value = palette[idx][channel];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
palette[idx][channel] = value;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputA = document.createElement('input');
|
||||||
|
inputA.type = 'number';
|
||||||
|
inputA.value = color[3];
|
||||||
|
inputA.min = 0;
|
||||||
|
inputA.max = 255;
|
||||||
|
inputA.setAttribute('data-palette-index', index);
|
||||||
|
inputA.setAttribute('data-palette-channel', '3');
|
||||||
|
inputA.addEventListener('change', (event) => {
|
||||||
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
||||||
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
||||||
|
const value = parseInt(event.target.value);
|
||||||
|
if(isNaN(value) || value < 0 || value > 255) {
|
||||||
|
event.target.value = palette[idx][channel];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
palette[idx][channel] = value;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorArea = document.createElement('div');
|
||||||
|
colorArea.style.display = 'inline-block';
|
||||||
|
colorArea.appendChild(inputR);
|
||||||
|
colorArea.appendChild(inputG);
|
||||||
|
colorArea.appendChild(inputB);
|
||||||
|
colorArea.appendChild(inputA);
|
||||||
|
|
||||||
|
const buttonArea = document.createElement('div');
|
||||||
|
buttonArea.style.display = 'inline-block';
|
||||||
|
const btnRemove = document.createElement('button');
|
||||||
|
btnRemove.textContent = '-';
|
||||||
|
btnRemove.setAttribute('data-palette-index', index);
|
||||||
|
btnRemove.addEventListener('click', () => {
|
||||||
|
palette.splice(index, 1);
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
buttonArea.appendChild(btnRemove);
|
||||||
|
|
||||||
|
const btnUp = document.createElement('button');
|
||||||
|
btnUp.textContent = '↑';
|
||||||
|
btnUp.setAttribute('data-palette-index', index);
|
||||||
|
btnUp.addEventListener('click', () => {
|
||||||
|
if(index === 0) return;
|
||||||
|
const temp = palette[index - 1];
|
||||||
|
palette[index - 1] = palette[index];
|
||||||
|
palette[index] = temp;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
buttonArea.appendChild(btnUp);
|
||||||
|
|
||||||
|
const btnDown = document.createElement('button');
|
||||||
|
btnDown.textContent = '↓';
|
||||||
|
btnDown.setAttribute('data-palette-index', index);
|
||||||
|
btnDown.addEventListener('click', () => {
|
||||||
|
if(index === palette.length - 1) return;
|
||||||
|
const temp = palette[index + 1];
|
||||||
|
palette[index + 1] = palette[index];
|
||||||
|
palette[index] = temp;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
buttonArea.appendChild(btnDown);
|
||||||
|
|
||||||
|
const footer = document.createElement('div');
|
||||||
|
footer.appendChild(colorArea);
|
||||||
|
footer.appendChild(buttonArea);
|
||||||
|
|
||||||
|
entry.appendChild(header);
|
||||||
|
entry.appendChild(footer);
|
||||||
|
elPaletteEntries.appendChild(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Image ready?
|
||||||
|
if(!imageWidth || !imageHeight) return;
|
||||||
|
|
||||||
|
// Update indexed image preview
|
||||||
|
elOutputPreview.width = imageWidth;
|
||||||
|
elOutputPreview.height = imageHeight;
|
||||||
|
const outputCtx = elOutputPreview.getContext('2d');
|
||||||
|
const outputImageData = outputCtx.createImageData(imageWidth, imageHeight);
|
||||||
|
const outputData = outputImageData.data;
|
||||||
|
indexedImage.forEach((index, i) => {
|
||||||
|
let color;
|
||||||
|
if(palette.length <= index) {
|
||||||
|
elOutputError.textContent = `Indexed image references non-existent palette index ${index}.`;
|
||||||
|
elOutputError.style.display = 'block';
|
||||||
|
color = [0, 0, 0, 0];
|
||||||
|
} else {
|
||||||
|
color = palette[index];
|
||||||
|
}
|
||||||
|
outputData[i * 4] = color[0];
|
||||||
|
outputData[i * 4 + 1] = color[1];
|
||||||
|
outputData[i * 4 + 2] = color[2];
|
||||||
|
outputData[i * 4 + 3] = color[3];
|
||||||
|
});
|
||||||
|
outputCtx.putImageData(outputImageData, 0, 0);
|
||||||
|
|
||||||
|
if(elOutputError.style.display === 'none') {;
|
||||||
|
const unusedPaletteIndexes = palette.map((color, index) => {
|
||||||
|
return indexedImage.includes(index) ? null : index;
|
||||||
|
}).filter(Boolean);
|
||||||
|
if(unusedPaletteIndexes.length > 0) {
|
||||||
|
elOutputError.textContent = `Image does not use palette colors; ${unusedPaletteIndexes.join(', ')}.`;
|
||||||
|
elOutputError.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescale canvas
|
||||||
|
const scale = parseInt(elPreviewScale.value) || 1;
|
||||||
|
elOutputPreview.style.width = `${imageWidth * scale}px`;
|
||||||
|
elOutputPreview.style.height = `${imageHeight * scale}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImage = img => {
|
||||||
|
if(!img) return onBadFile('Failed to load image');
|
||||||
|
|
||||||
|
elError.style.display = 'none';
|
||||||
|
imageWidth = img.width;
|
||||||
|
imageHeight = img.height;
|
||||||
|
indexedImage = [];
|
||||||
|
|
||||||
|
// Update input preview
|
||||||
|
elInputPreview.width = imageWidth;
|
||||||
|
elInputPreview.height = imageHeight;
|
||||||
|
const ctx = elInputPreview.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// Source palette from input image
|
||||||
|
palette = sourcePaletteFromImage(img);
|
||||||
|
|
||||||
|
// Index the image
|
||||||
|
const imageData = ctx.getImageData(0, 0, imageWidth, imageHeight);
|
||||||
|
const data = imageData.data;
|
||||||
|
for(let i = 0; i < data.length; i += 4) {
|
||||||
|
const color = [ data[i], data[i + 1], data[i + 2], data[i + 3] ];
|
||||||
|
const colorIndex = palette.findIndex(c => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3]);
|
||||||
|
indexedImage.push(colorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPalettizedImage = dataView => {
|
||||||
|
if(!dataView) return onBadFile('Failed to load image');
|
||||||
|
|
||||||
|
// Ensure DPT and ver is 1
|
||||||
|
if(dataView.getUint8(0) !== 0x44 || dataView.getUint8(1) !== 0x50 || dataView.getUint8(2) !== 0x54 || dataView.getUint8(3) !== 0x01) {
|
||||||
|
return onBadFile('Invalid DPT file. Expected header "DPT" and version 1.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width and Height
|
||||||
|
elError.style.display = 'none';
|
||||||
|
imageWidth = dataView.getUint32(3);
|
||||||
|
imageHeight = dataView.getUint32(7);
|
||||||
|
|
||||||
|
if(imageWidth <= 0 || imageHeight <= 0) {
|
||||||
|
return onBadFile('Invalid image dimensions in DPT file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dataView.byteLength < 11 + imageWidth * imageHeight) {
|
||||||
|
return onBadFile('DPT file does not contain enough data for the specified dimensions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueIndexes = []
|
||||||
|
|
||||||
|
// Image data
|
||||||
|
indexedImage = [];
|
||||||
|
for(let i = 11; i < dataView.byteLength; i++) {
|
||||||
|
const index = dataView.getUint8(i);
|
||||||
|
if(!uniqueIndexes.includes(index)) uniqueIndexes.push(index);
|
||||||
|
indexedImage.push(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const adhocPalette = [];
|
||||||
|
for(let i = 0; i < uniqueIndexes.length; i++) {
|
||||||
|
const index = uniqueIndexes[i];
|
||||||
|
// Get the most different possible color for this index
|
||||||
|
const color = [
|
||||||
|
(index * 37) % 256,
|
||||||
|
(index * 61) % 256,
|
||||||
|
(index * 97) % 256,
|
||||||
|
255
|
||||||
|
];
|
||||||
|
adhocPalette[index] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
elInputPreview.width = imageWidth;
|
||||||
|
elInputPreview.height = imageHeight;
|
||||||
|
const ctx = elInputPreview.getContext('2d');
|
||||||
|
const imageData = ctx.createImageData(imageWidth, imageHeight);
|
||||||
|
const data = imageData.data;
|
||||||
|
indexedImage.forEach((index, i) => {
|
||||||
|
const color = adhocPalette[index] || [0, 0, 0, 0];
|
||||||
|
data[i * 4] = color[0];
|
||||||
|
data[i * 4 + 1] = color[1];
|
||||||
|
data[i * 4 + 2] = color[2];
|
||||||
|
data[i * 4 + 3] = color[3];
|
||||||
|
});
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
updateOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBadFile = err => {
|
||||||
|
elError.textContent = err;
|
||||||
|
elError.style.display = 'block';
|
||||||
|
updateOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFile = file => {
|
||||||
|
// Reset preview
|
||||||
|
elInputPreview.width = 0;
|
||||||
|
elInputPreview.height = 0;
|
||||||
|
image = null;
|
||||||
|
|
||||||
|
if(!file) {
|
||||||
|
onImage(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file is image, load as image.
|
||||||
|
if(file.type.startsWith('image/')) {
|
||||||
|
elError.style.display = 'none';
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => onImage(img);
|
||||||
|
img.onerror = () => onImage(null);
|
||||||
|
img.src = event.target.result;
|
||||||
|
};
|
||||||
|
reader.onerror = () => onImage(null);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else if(file.name.endsWith('.dpt')) {
|
||||||
|
elError.style.display = 'none';
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const arrayBuffer = event.target.result;
|
||||||
|
const dataView = new DataView(arrayBuffer);
|
||||||
|
onPalettizedImage(dataView);
|
||||||
|
};
|
||||||
|
reader.onerror = () => onPalettizedImage(null);
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBadFile('Selected file is not a supported image type.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listeners
|
||||||
|
btnBackgroundWhite.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'white';
|
||||||
|
});
|
||||||
|
btnBackgroundTransparent.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'black';
|
||||||
|
});
|
||||||
|
btnBackgroundCheckerboard.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px';
|
||||||
|
});
|
||||||
|
btnBackgroundMagenta.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'magenta';
|
||||||
|
});
|
||||||
|
btnBackgroundBlue.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'blue';
|
||||||
|
});
|
||||||
|
btnBackgroundGreen.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'green';
|
||||||
|
});
|
||||||
|
|
||||||
|
elFile.addEventListener('change', (event) => {
|
||||||
|
onFile(event?.target?.files[0]);
|
||||||
|
});
|
||||||
|
elPaletteAdd.addEventListener('click', () => {
|
||||||
|
// Add new random color to palette
|
||||||
|
const newColor = [ Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), 255 ];
|
||||||
|
palette.push(newColor);
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
elPreviewScale.addEventListener('change', () => {
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
elClearPalette.addEventListener('click', () => {
|
||||||
|
palette = [];
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
elOptimizePalette.addEventListener('click', () => {
|
||||||
|
// Remove unused colors and duplicates from the palette. Treat A=0 as equal
|
||||||
|
const newPalette = [];
|
||||||
|
const indexMap = {};
|
||||||
|
palette.forEach((color, index) => {
|
||||||
|
// Check if color is already in new palette
|
||||||
|
const existingIndex = newPalette.findIndex(c => {
|
||||||
|
if(c[3] === 0 && color[3] === 0) return true;
|
||||||
|
return c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3];
|
||||||
|
});
|
||||||
|
if(existingIndex !== -1) {
|
||||||
|
indexMap[index] = existingIndex;
|
||||||
|
} else {
|
||||||
|
indexMap[index] = newPalette.length;
|
||||||
|
newPalette.push(color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
palette = newPalette;
|
||||||
|
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
elAppendPalette.addEventListener('click', () => {
|
||||||
|
// Open file picker
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.addEventListener('change', (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if(!file) return;
|
||||||
|
|
||||||
|
// Accept either images or .dpf files
|
||||||
|
if(file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const newColors = sourcePaletteFromImage(img);
|
||||||
|
palette = palette.concat(newColors);
|
||||||
|
updateOutput();
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
alert('Failed to load image for palette appending.');
|
||||||
|
};
|
||||||
|
img.src = event.target.result;
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
alert('Failed to load image for palette appending.');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
return;
|
||||||
|
} else if(file.name.endsWith('.dpf')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const arrayBuffer = event.target.result;
|
||||||
|
const dataView = new DataView(arrayBuffer);
|
||||||
|
|
||||||
|
// Validate header
|
||||||
|
if(dataView.getUint8(0) !== 0x44 || dataView.getUint8(1) !== 0x50 || dataView.getUint8(2) !== 0x46) {
|
||||||
|
alert('Invalid DPF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorCount = dataView.getUint32(4);
|
||||||
|
const expectedLength = 4 * colorCount + 8;// 3 DPF, 1 ver, 4 color count
|
||||||
|
if(arrayBuffer.byteLength !== expectedLength) {
|
||||||
|
alert('DPF file size does not match expected color count.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = 8;
|
||||||
|
for(let i = 0; i < colorCount; i++) {
|
||||||
|
const r = dataView.getUint8(offset);
|
||||||
|
const g = dataView.getUint8(offset + 1);
|
||||||
|
const b = dataView.getUint8(offset + 2);
|
||||||
|
const a = dataView.getUint8(offset + 3);
|
||||||
|
palette.push([r, g, b, a]);
|
||||||
|
offset += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutput();
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
alert('Failed to load DPF file for palette appending.');
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
alert('Unsupported file type for palette appending. Please use an image file.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDownloadPalette.addEventListener('click', () => {
|
||||||
|
// Create Dusk Palette File (.dpf)
|
||||||
|
const header = new Uint8Array([0x44, 0x50, 0x46, 0x01]); // 'DPF' + version 1 + number of colors (4 bytes)
|
||||||
|
|
||||||
|
const colorCount = palette.length;
|
||||||
|
const colorCountBytes = new Uint8Array(4);
|
||||||
|
colorCountBytes[0] = (colorCount >> 24) & 0xFF;
|
||||||
|
colorCountBytes[1] = (colorCount >> 16) & 0xFF;
|
||||||
|
colorCountBytes[2] = (colorCount >> 8) & 0xFF;
|
||||||
|
colorCountBytes[3] = colorCount & 0xFF;
|
||||||
|
|
||||||
|
// add color data (palette.length * 4 bytes)
|
||||||
|
const colorData = new Uint8Array(palette.length * 4);
|
||||||
|
palette.forEach((color, index) => {
|
||||||
|
colorData[index * 4] = color[0];
|
||||||
|
colorData[index * 4 + 1] = color[1];
|
||||||
|
colorData[index * 4 + 2] = color[2];
|
||||||
|
colorData[index * 4 + 3] = color[3];
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([header, colorCountBytes, colorData], { type: 'application/octet-stream' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'palette.dpf';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDownloadImage.addEventListener('click', () => {
|
||||||
|
const header = new Uint8Array([0x44, 0x50, 0x54, 0x01]); // 'DPT' + version 1
|
||||||
|
|
||||||
|
// Width
|
||||||
|
const widthBytes = new Uint8Array(4);
|
||||||
|
widthBytes[0] = (imageWidth >> 24) & 0xFF;
|
||||||
|
widthBytes[1] = (imageWidth >> 16) & 0xFF;
|
||||||
|
widthBytes[2] = (imageWidth >> 8) & 0xFF;
|
||||||
|
widthBytes[3] = imageWidth & 0xFF;
|
||||||
|
|
||||||
|
// Height
|
||||||
|
const heightBytes = new Uint8Array(4);
|
||||||
|
heightBytes[0] = (imageHeight >> 24) & 0xFF;
|
||||||
|
heightBytes[1] = (imageHeight >> 16) & 0xFF;
|
||||||
|
heightBytes[2] = (imageHeight >> 8) & 0xFF;
|
||||||
|
heightBytes[3] = imageHeight & 0xFF;
|
||||||
|
|
||||||
|
// add indexed image data (imageWidth * imageHeight bytes)
|
||||||
|
const imageData = new Uint8Array(indexedImage.length);
|
||||||
|
indexedImage.forEach((index, i) => {
|
||||||
|
imageData[i] = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([header, widthBytes, heightBytes, imageData], { type: 'application/octet-stream' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'indexed_image.dpt';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateOutput();
|
||||||
|
btnBackgroundCheckerboard.click();
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
from tools.util.type import detectType, stringToCType, typeToCType
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Story CSV to .h defines")
|
parser = argparse.ArgumentParser(description="Story CSV to .h defines")
|
||||||
parser.add_argument("--csv", required=True, help="Path to story CSV file")
|
parser.add_argument("--csv", required=True, help="Path to story CSV file")
|
||||||
|
|||||||
223
tools/tile-joiner.html
Normal file
223
tools/tile-joiner.html
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dusk Tools / Tile Joiner</title>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Dusk Tile Joiner</h1>
|
||||||
|
<p>
|
||||||
|
Joins multiple tiles together into a single tileset image. Optimizing and
|
||||||
|
allowing it to work on multiple platforms. Output width and height will
|
||||||
|
always be a power of two. This tool will take a while to process images,
|
||||||
|
since all images need to be loaded into memory.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<input type="file" data-file-input multiple />
|
||||||
|
</div>
|
||||||
|
<p data-file-error style="color:red;display:none;"></p>
|
||||||
|
<textarea readonly data-input-information rows="10" style="width:500px"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Preview Background:
|
||||||
|
<button data-page-bg-white>White</button>
|
||||||
|
<button data-page-bg-transparent>Black</button>
|
||||||
|
<button data-page-bg-checkerboard>Checkerboard</button>
|
||||||
|
<button data-page-bg-magenta>Magenta</button>
|
||||||
|
<button data-page-bg-blue>Blue</button>
|
||||||
|
<button data-page-bg-green>Green</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Joined Preview</h2>
|
||||||
|
<div>
|
||||||
|
<button data-download>Download Joined Image</button>
|
||||||
|
</div>
|
||||||
|
<canvas data-preview-canvas style="border:1px solid black;"></canvas>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const elFileInput = document.querySelector('[data-file-input]');
|
||||||
|
const elFileError = document.querySelector('[data-file-error]');
|
||||||
|
const elInputInformation = document.querySelector('[data-input-information]');
|
||||||
|
const elPreviewCanvas = document.querySelector('[data-preview-canvas]');
|
||||||
|
const btnDownload = document.querySelector('[data-download]');
|
||||||
|
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
|
||||||
|
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
|
||||||
|
const btnBackgroundCheckerboard = document.querySelector('[data-page-bg-checkerboard]');
|
||||||
|
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
|
||||||
|
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
|
||||||
|
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
|
||||||
|
|
||||||
|
let images = {};
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const nextPowerOfTwo = n => {
|
||||||
|
if(n <= 0) return 1;
|
||||||
|
return 2 ** Math.ceil(Math.log2(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBadImages = error => {
|
||||||
|
elInputInformation.value = '';
|
||||||
|
elFileError.style.display = 'block';
|
||||||
|
elFileError.textContent = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImages = () => {
|
||||||
|
if(!images || Object.keys(images).length <= 1) {
|
||||||
|
return onBadImages('Please select 2 or more image images.');
|
||||||
|
}
|
||||||
|
|
||||||
|
elFileError.style.display = 'none';
|
||||||
|
|
||||||
|
let strInfo = `Selected ${Object.keys(images).length} images:\n`;
|
||||||
|
for(const [name, img] of Object.entries(images)) {
|
||||||
|
strInfo += `- ${name}: ${img.width}x${img.height}\n`;
|
||||||
|
}
|
||||||
|
elInputInformation.value = strInfo;
|
||||||
|
|
||||||
|
// Determine output width and height to pack images together, must be a
|
||||||
|
// power of two. If all images share a given axis (width/height) and that
|
||||||
|
// axis is already a power of two, use that.
|
||||||
|
const firstWidth = Object.values(images)[0].width;
|
||||||
|
const firstHeight = Object.values(images)[0].height;
|
||||||
|
let allImagesShareWidth = Object.values(images).every(img => img.width === firstWidth);
|
||||||
|
let allImagesShareHeight = Object.values(images).every(img => img.height === firstHeight);
|
||||||
|
|
||||||
|
let outputHeight, outputWidth;
|
||||||
|
|
||||||
|
if(allImagesShareWidth && nextPowerOfTwo(firstWidth) === firstWidth) {
|
||||||
|
outputWidth = firstWidth;
|
||||||
|
outputHeight = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.height, 0));
|
||||||
|
} else if(allImagesShareHeight && nextPowerOfTwo(firstHeight) === firstHeight) {
|
||||||
|
outputHeight = firstHeight;
|
||||||
|
outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0));
|
||||||
|
} else {
|
||||||
|
onBadImages('All images must share the same width or height, and that dimension must be a power of two.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update preview
|
||||||
|
elPreviewCanvas.width = outputWidth;
|
||||||
|
elPreviewCanvas.height = outputHeight;
|
||||||
|
const ctx = elPreviewCanvas.getContext('2d');
|
||||||
|
let currentX = 0;
|
||||||
|
let currentY = 0;
|
||||||
|
for(const img of Object.values(images)) {
|
||||||
|
ctx.drawImage(img, currentX, currentY);
|
||||||
|
if(allImagesShareWidth) {
|
||||||
|
currentY += img.height;
|
||||||
|
} else {
|
||||||
|
currentX += img.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFiles = async files => {
|
||||||
|
images = {};
|
||||||
|
|
||||||
|
if(!files || files.length <= 1) {
|
||||||
|
return onBadImages('Please select 2 or more image files.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let strError = '';
|
||||||
|
for(const file of files) {
|
||||||
|
if(!file.type.startsWith('image/')) {
|
||||||
|
strError += `File is not an image: ${file.name}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(strError) {
|
||||||
|
return onBadImages(strError);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileImages = await Promise.all(Array.from(files).map(file => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = event => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
images[file.name] = img;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(`Failed to load image: ${file.name}`);
|
||||||
|
}
|
||||||
|
img.src = event.target.result;
|
||||||
|
}
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(`Failed to read file: ${file.name}`);
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
} catch(error) {
|
||||||
|
return onBadImages(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
onImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listeners
|
||||||
|
elFileInput.addEventListener('change', event => {
|
||||||
|
onFiles(event?.target?.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnBackgroundWhite.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'white';
|
||||||
|
});
|
||||||
|
btnBackgroundTransparent.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'black';
|
||||||
|
});
|
||||||
|
btnBackgroundCheckerboard.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px';
|
||||||
|
});
|
||||||
|
btnBackgroundMagenta.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'magenta';
|
||||||
|
});
|
||||||
|
btnBackgroundBlue.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'blue';
|
||||||
|
});
|
||||||
|
btnBackgroundGreen.addEventListener('click', () => {
|
||||||
|
document.body.style.background = 'green';
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDownload.addEventListener('click', () => {
|
||||||
|
if(!images || Object.keys(images).length <= 1) {
|
||||||
|
return onBadImages('Please select 2 or more image files before downloading.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = 'joined.png';
|
||||||
|
link.href = elPreviewCanvas.toDataURL();
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnBackgroundCheckerboard.click();
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
391
tools/tile-slicer.html
Normal file
391
tools/tile-slicer.html
Normal file
File diff suppressed because one or more lines are too long
@@ -1,44 +0,0 @@
|
|||||||
def detectType(value: str) -> str:
|
|
||||||
val = value.strip()
|
|
||||||
# Boolean check
|
|
||||||
if val.lower() in {'true', 'false'}:
|
|
||||||
return 'Boolean'
|
|
||||||
|
|
||||||
# Int check
|
|
||||||
try:
|
|
||||||
int(val)
|
|
||||||
return 'Int'
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Float check
|
|
||||||
try:
|
|
||||||
float(val)
|
|
||||||
return 'Float'
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Default to String
|
|
||||||
return 'String'
|
|
||||||
|
|
||||||
def typeToCType(valType: str) -> str:
|
|
||||||
if valType == 'Int':
|
|
||||||
return 'int'
|
|
||||||
elif valType == 'Float':
|
|
||||||
return 'float'
|
|
||||||
elif valType == 'Boolean':
|
|
||||||
return 'bool'
|
|
||||||
else:
|
|
||||||
return 'char_t*'
|
|
||||||
|
|
||||||
def stringToCType(value: str) -> str:
|
|
||||||
valType = detectType(value)
|
|
||||||
if valType == 'Int':
|
|
||||||
return str(int(value))
|
|
||||||
elif valType == 'Float':
|
|
||||||
return str(float(value))
|
|
||||||
elif valType == 'Boolean':
|
|
||||||
return 'true' if value.lower() == 'true' else 'false'
|
|
||||||
else:
|
|
||||||
escaped = value.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
return f'"{escaped}"'
|
|
||||||
Reference in New Issue
Block a user