Dusk texture creator

This commit is contained in:
YourWishes committed 2026-04-13 19:51:11 -05:00
1 parent 4b3826edd9
commit 5a651d2d1f
39 files changed
+1398 -2655

No files matched your search

+1 -1
View File
@@ -83,7 +83,6 @@ assets/borrowed
.VSCode*
/vita
._*
*~
@@ -106,3 +105,4 @@ yarn.lock
/build2
/build*
/assets/test
/tools_old
-3
View File
@@ -58,9 +58,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
main.c
)
# Defs
dusk_env_to_h(duskdefs.env duskdefs.h)
# Subdirs
add_subdirectory(assert)
add_subdirectory(asset)
+1 -1
View File
@@ -21,7 +21,7 @@ add_subdirectory(texture)
# Color definitions
dusk_run_python(
dusk_color_defs
tools.display.color.csv
tools.color.csv
--csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
)
-45
View File
@@ -1,45 +0,0 @@
# Copyright (c) 2025 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
ENTITY_DIR_SOUTH = 0
ENTITY_DIR_WEST = 1
ENTITY_DIR_EAST = 2
ENTITY_DIR_NORTH = 3
ENTITY_COUNT = 128
ENTITY_TYPE_NULL = 0
ENTITY_TYPE_PLAYER = 1
ENTITY_TYPE_NPC = 2
ENTITY_TYPE_COUNT = 3
CHUNK_WIDTH = 16
CHUNK_HEIGHT = 16
CHUNK_DEPTH = 4
# CHUNK_VERTEX_COUNT_MAX = QUAD_VERTEXES * CHUNK_WIDTH * CHUNK_HEIGHT * 4
CHUNK_VERTEX_COUNT_MAX=6144
CHUNK_MESH_COUNT_MAX = 14
CHUNK_ENTITY_COUNT_MAX = 8
TILE_WIDTH = 16.0
TILE_HEIGHT = 16.0
TILE_DEPTH = 16.0
TILE_SHAPE_NULL = 0
TILE_SHAPE_FLOOR = 1
TILE_SHAPE_RAMP_SOUTH = 2
TILE_SHAPE_RAMP_WEST = 3
TILE_SHAPE_RAMP_EAST = 4
TILE_SHAPE_RAMP_NORTH = 5
TILE_SHAPE_RAMP_SOUTHWEST = 6
TILE_SHAPE_RAMP_SOUTHEAST = 7
TILE_SHAPE_RAMP_NORTHWEST = 8
TILE_SHAPE_RAMP_NORTHEAST = 9
RPG_CAMERA_FOV = 70
RPG_CAMERA_PIXELS_PER_UNIT = 1.0
RPG_CAMERA_Z_OFFSET = 24.0
ASSET_LANG_CHUNK_CHAR_COUNT = 6144
+1 -1
View File
@@ -14,6 +14,6 @@ dusk_run_python(
dusk_story_defs
tools.story.csv
--csv ${CMAKE_CURRENT_SOURCE_DIR}/storyflag.csv
--header-file ${DUSK_GENERATED_HEADERS_DIR}/story/storyflagvalue.h
--output ${DUSK_GENERATED_HEADERS_DIR}/story/storyflagvalue.h
)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_story_defs)
+1 -2
View File
@@ -1,7 +1,6 @@
# Copyright (c) 2025 Dominic Msters
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(run_python)
add_subdirectory(env_to_h)
+79
View File
@@ -0,0 +1,79 @@
import argparse
import csv
import os
parser = argparse.ArgumentParser(description="Color CSV to .h defines")
parser.add_argument("--csv", required=True, help="Path to color CSV file")
parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args()
# Load colors
colors = {}
with open(args.csv, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if "name" not in reader.fieldnames:
raise ValueError("CSV must have a 'name' column")
for row in reader:
name = row["name"]
r, g, b = row["r"], row["g"], row["b"]
a = row["a"] if "a" in row and row["a"] != "" else "1.0"
for ch in (r, g, b, a):
if not (0.0 <= float(ch) <= 1.0):
raise ValueError(f"Color channel {ch!r} for '{name}' out of range [0.0, 1.0]")
colors[name] = (r, g, b, a)
# Build output
out = [
"#pragma once",
'#include "dusk.h"',
"",
"typedef float_t colorchannelf_t;",
"typedef uint8_t colorchannel8_t;",
"",
"typedef struct { colorchannelf_t r, g, b; } color3f_t;",
"typedef struct { colorchannelf_t r, g, b, a; } color4f_t;",
"typedef struct { colorchannel8_t r, g, b; } color3b_t;",
"typedef struct { colorchannel8_t r, g, b, a; } color4b_t;",
"typedef color4b_t color_t;",
"",
"#define color3f(r, g, b) ((color3f_t){ r, g, b })",
"#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })",
"#define color3b(r, g, b) ((color3b_t){ r, g, b })",
"#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })",
"#define color(r, g, b, a) color4b(r, g, b, a)",
"#define colorHex(hex) color4b(((hex >> 24) & 0xFF), ((hex >> 16) & 0xFF), ((hex >> 8) & 0xFF), (hex & 0xFF))",
"",
]
lua = []
for name, (r, g, b, a) in colors.items():
r8, g8, b8, a8 = (int(float(ch) * 255) for ch in (r, g, b, a))
macro = "COLOR_" + name.upper()
camel = "".join(p[0].upper() + p[1:].lower() for p in name.split("_"))
out += [
f"// {name}",
f"#define {macro}_4B color4b({r8}, {g8}, {b8}, {a8})",
f"#define {macro}_3B color3b({r8}, {g8}, {b8})",
f"#define {macro}_3F color3f({r}f, {g}f, {b}f)",
f"#define {macro}_4F color4f({r}f, {g}f, {b}f, {a}f)",
f"#define {macro} {macro}_4B",
"",
]
lua += [
f"function color{camel}()",
f" return color({r8}, {g8}, {b8}, {a8})",
"end",
"",
]
out.append("// Lua color functions")
out.append("#define COLOR_SCRIPT \\")
for line in "\n".join(lua).rstrip().splitlines():
out.append(f' "{line}\\n" \\')
out[-1] = out[-1].rstrip(" \\")
out.append("")
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
f.write("\n".join(out))
-106
View File
@@ -1,106 +0,0 @@
import argparse
import os
import csv
parser = argparse.ArgumentParser(description="Color CSV to .h defines")
parser.add_argument("--csv", required=True, help="Path to color CSV file")
parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args()
def csvIdToEnumName(inputId):
return "INPUT_ACTION_" + inputId.upper()
# Load up CSV file.
outHeader = "#pragma once\n"
outHeader += '#include "dusk.h"\n\n'
outHeader += f"typedef float_t colorchannelf_t;\n"
colors = {}
with open(args.csv, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
# CSV must have id column.
if "name" not in reader.fieldnames:
raise Exception("CSV file must have 'name' column")
# For each color, by name...
for row in reader:
name = row["name"]
r = row["r"]
g = row["g"]
b = row["b"]
a = row["a"] if "a" in row and row["a"] != "" else "1.0"
# Ensure values are between 0.0 and 1.0
for channelValue in (r, g, b, a):
fvalue = float(channelValue)
if fvalue < 0.0 or fvalue > 1.0:
raise Exception(f"Color channel value {channelValue} for color {name} is out of range (0.0 to 1.0)")
colors[name] = (r, g, b, a)
# Prep output header
outHeader = "#pragma once\n"
outHeader += '#include "dusk.h"\n\n'
# Typedefs for float and uin8t color channels
outHeader += f"typedef float_t colorchannelf_t;\n"
outHeader += f"typedef uint8_t colorchannel8_t;\n\n"
# Typedefs for 3 and 4 channel colors in both float and uint8_t
outHeader += f"typedef struct {{ colorchannelf_t r, g, b; }} color3f_t;\n"
outHeader += f"typedef struct {{ colorchannelf_t r, g, b, a; }} color4f_t;\n"
outHeader += f"typedef struct {{ colorchannel8_t r, g, b; }} color3b_t;\n"
outHeader += f"typedef struct {{ colorchannel8_t r, g, b, a; }} color4b_t;\n"
outHeader += "typedef color4b_t color_t;\n\n"# Preferred format.
outHeader += "#define color3f(r, g, b) ((color3f_t){ r, g, b })\n"
outHeader += "#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })\n"
outHeader += "#define color3b(r, g, b) ((color3b_t){ r, g, b })\n"
outHeader += "#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })\n\n"
outHeader += "#define color(r, g, b, a) color4b(r, g, b, a)\n\n" # Preferred format.
outHeader += "#define colorHex(hex) color4b(((hex >> 24) & 0xFF), ((hex >> 16) & 0xFF), ((hex >> 8) & 0xFF), (hex & 0xFF))\n\n"
luaScript = ""
# Define each color, in each format
for color_name, (r, g, b, a) in colors.items():
outHeader += f"// Color: {color_name}\n"
r8 = int(float(r) * 255)
g8 = int(float(g) * 255)
b8 = int(float(b) * 255)
a8 = int(float(a) * 255)
macro_name = "COLOR_" + color_name.upper()
outHeader += f"#define {macro_name}_4B color4b({r8}, {g8}, {b8}, {a8})\n"
outHeader += f"#define {macro_name}_3B color3b({r8}, {g8}, {b8})\n"
outHeader += f"#define {macro_name}_3F color3f({r}f, {g}f, {b}f)\n"
outHeader += f"#define {macro_name}_4F color4f({r}f, {g}f, {b}f, {a}f)\n"
outHeader += f"#define {macro_name} {macro_name}_4B\n\n" # Preferred format.
nameSplit = color_name.split("_")
camelFirstLetter = ""
for part in nameSplit:
camelFirstLetter += part[0].upper() + part[1:].lower()
luaName = "color" + camelFirstLetter
luaScript += f"function {luaName}()\n"
luaScript += f" return color({r8}, {g8}, {b8}, {a8})\n"
luaScript += "end\n\n"
outHeader += "// Lua color functions\n"
outHeader += "#define COLOR_SCRIPT \\\n"
# Stringify and put each line with a backslash
for line in luaScript.splitlines():
outHeader += f' "{line}\\n" \\\n'
outHeader = outHeader.rstrip(" \\\n") + "\n" # Remove last backslash and add newline
# Write to output file
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as outFile:
outFile.write(outHeader)
+173
View File
@@ -0,0 +1,173 @@
"use strict";
// DTF – Dusk Texture Format
//
// Header (13 bytes):
// [0–2] "DTF" magic
// [3] 0x01 version
// [4–7] uint32 width (little-endian)
// [8–11] uint32 height (little-endian)
// [12] uint8 format
//
// Formats:
// 0x01 Alpha – 1 byte per pixel (alpha channel only)
// 0x03 RGB – 3 bytes per pixel (no alpha)
// 0x04 RGBA – 4 bytes per pixel
//
// Followed by width × height × bpp bytes of tightly-packed pixel data.
const DTF = (() => {
const MAGIC = [0x44, 0x54, 0x46]; // "DTF"
const VERSION = 0x01;
const FORMAT_ALPHA = 0x01;
const FORMAT_RGB = 0x03;
const FORMAT_RGBA = 0x04;
const HEADER_SIZE = 13;
// Bytes per pixel for each format.
const BPP = {
[FORMAT_ALPHA]: 1,
[FORMAT_RGB]: 3,
[FORMAT_RGBA]: 4,
};
// Encode RGBA source pixels into a DTF ArrayBuffer at the given format.
// 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;
const bpp = BPP[format];
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);
const bytes = new Uint8Array(buf);
const view = new DataView(buf);
bytes[0] = MAGIC[0];
bytes[1] = MAGIC[1];
bytes[2] = MAGIC[2];
bytes[3] = VERSION;
view.setUint32(4, width, true);
view.setUint32(8, height, true);
bytes[12] = format;
let dst = HEADER_SIZE;
for (let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
bytes[dst++] = redAsAlpha ? src[o] : src[o + 3];
break;
case FORMAT_RGB:
bytes[dst++] = src[o];
bytes[dst++] = src[o + 1];
bytes[dst++] = src[o + 2];
break;
default: // FORMAT_RGBA
bytes[dst++] = src[o];
bytes[dst++] = src[o + 1];
bytes[dst++] = src[o + 2];
bytes[dst++] = src[o + 3];
}
}
return buf;
}
// Decode a DTF ArrayBuffer. Always returns RGBA pixel data for internal use.
// Alpha-format files decode as {R=0, G=0, B=0, A=alpha} so the alpha channel
// is preserved and toImageData() can display it correctly.
function decode(buffer) {
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]) {
throw new Error("Invalid DTF magic bytes – not a DTF file");
}
const version = bytes[3];
if (version !== VERSION) {
throw new Error(`Unsupported DTF version: 0x${version.toString(16).padStart(2, "0")}`);
}
const width = view.getUint32(4, true);
const height = view.getUint32(8, true);
const format = bytes[12];
const bpp = BPP[format];
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) {
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++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
rgba[o] = rgba[o + 1] = rgba[o + 2] = 0;
rgba[o + 3] = bytes[src++];
break;
case FORMAT_RGB:
rgba[o] = bytes[src++];
rgba[o + 1] = bytes[src++];
rgba[o + 2] = bytes[src++];
rgba[o + 3] = 255;
break;
default: // FORMAT_RGBA
rgba[o] = bytes[src++];
rgba[o + 1] = bytes[src++];
rgba[o + 2] = bytes[src++];
rgba[o + 3] = bytes[src++];
}
}
return { width, height, format, data: rgba };
}
// Convert RGBA source pixels to a display-ready ImageData for the given format.
// Shows exactly how the texture will look after a DTF encode/decode round-trip.
// Alpha → grayscale from alpha channel (or red if redAsAlpha), out-alpha=255
// RGB → discard alpha, fully opaque
// RGBA → pass-through
function toImageData(width, height, rgbaData, format, redAsAlpha) {
const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData);
const out = new Uint8ClampedArray(width * height * 4);
for (let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
out[o] = out[o + 1] = out[o + 2] = redAsAlpha ? src[o] : src[o + 3];
out[o + 3] = 255;
break;
case FORMAT_RGB:
out[o] = src[o];
out[o + 1] = src[o + 1];
out[o + 2] = src[o + 2];
out[o + 3] = 255;
break;
default: // FORMAT_RGBA
out[o] = src[o];
out[o + 1] = src[o + 1];
out[o + 2] = src[o + 2];
out[o + 3] = src[o + 3];
}
}
return new ImageData(out, width, height);
}
return Object.freeze({
encode, decode, toImageData,
FORMAT_ALPHA, FORMAT_RGB, FORMAT_RGBA,
VERSION, HEADER_SIZE, BPP,
});
})();
+55
View File
@@ -0,0 +1,55 @@
"use strict";
// DuskPNG – PNG export using pngjs (window.PNG)
// Falls back to canvas.toBlob if pngjs is unavailable.
const DuskPNG = (() => {
function _pngAvailable() {
return typeof PNG !== "undefined" && PNG.sync && typeof PNG.sync.write === "function";
}
// 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;
const png = new PNG({ width, height });
const src = rgbaData instanceof Uint8ClampedArray
? rgbaData
: new Uint8ClampedArray(rgbaData);
for (let i = 0; i < src.length; i++) {
png.data[i] = src[i];
}
return PNG.sync.write(png); // returns a Buffer (Uint8Array subclass)
}
// Trigger a browser download of the RGBA data as a PNG file.
function download(filename, width, height, rgbaData) {
const buf = encode(width, height, rgbaData);
if (buf) {
// pngjs path
const blob = new Blob([buf], { type: "image/png" });
_triggerDownload(URL.createObjectURL(blob), filename);
} else {
// Canvas fallback
const canvas = Object.assign(document.createElement("canvas"), { width, height });
const ctx = canvas.getContext("2d");
const src = rgbaData instanceof Uint8ClampedArray
? rgbaData
: new Uint8ClampedArray(rgbaData);
ctx.putImageData(new ImageData(src, width, height), 0, 0);
canvas.toBlob(blob => _triggerDownload(URL.createObjectURL(blob), filename), "image/png");
}
}
function _triggerDownload(url, filename) {
const a = Object.assign(document.createElement("a"), { href: url, download: filename });
a.click();
URL.revokeObjectURL(url);
}
return Object.freeze({ encode, download });
})();
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dusk Editor Tools</title>
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<header class="site-header">
<span class="logo">Dusk <span>Editor</span></span>
<nav>
<a href="/" class="active">Home</a>
</nav>
</header>
<main class="page">
<section class="hero">
<h1>Editor Tools</h1>
<p>Asset creation and data authoring tools for the Dusk game engine.</p>
</section>
<section class="section">
<div class="section-label">Tools</div>
<div class="tool-grid" id="tool-grid">
<a class="tool-card" href="/texture/">
<div class="tool-name">Texture Creator</div>
<div class="tool-desc">Load PNG, JPG, or DTF files and export to the Dusk Texture Format (.dtf) or PNG.</div>
</a>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,34 @@
/* Component – Buttons */
.btn {
display: block;
width: 100%;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
padding: 0.4rem 0.75rem;
font-size: 0.825rem;
cursor: pointer;
text-align: center;
font-family: inherit;
transition: background var(--speed), border-color var(--speed);
}
.btn + .btn {
margin-top: 0.375rem;
}
.btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent-dim);
border-color: var(--accent);
}
.btn-primary:hover:not(:disabled) {
background: var(--accent);
}
+38
View File
@@ -0,0 +1,38 @@
/* Component – Tool cards and empty state */
.tool-card {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
transition: border-color var(--speed);
text-decoration: none;
color: inherit;
}
.tool-card:hover {
text-decoration: none;
}
.tool-card .tool-name {
font-weight: 600;
font-size: 0.95rem;
}
.tool-card .tool-desc {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.5;
}
.empty-state {
border-style: dashed;
padding: 3rem;
text-align: center;
color: var(--text-muted);
}
.empty-state p {
margin-top: 0.5rem;
font-size: 0.875rem;
}
@@ -0,0 +1,43 @@
/* Component – File info and load area */
.file-name {
font-size: 0.775rem;
color: var(--text-muted);
word-break: break-all;
line-height: 1.4;
}
.info-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
.info-table td {
padding: 0.15rem 0;
vertical-align: top;
}
.info-table td:first-child {
color: var(--text-muted);
width: 50px;
font-size: 0.75rem;
padding-right: 0.75rem;
}
.load-area {
border: 1px dashed var(--border);
border-radius: var(--radius);
padding: 0.6rem;
text-align: center;
cursor: pointer;
font-size: 0.825rem;
color: var(--text-muted);
transition: border-color var(--speed), color var(--speed);
line-height: 1.5;
}
.load-area:hover,
.load-area.drag-over {
color: var(--text);
}
+43
View File
@@ -0,0 +1,43 @@
/* Component – Site header */
.site-header {
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
padding: 0 var(--gap);
display: flex;
align-items: center;
gap: 1rem;
height: 52px;
}
.site-header .logo {
font-weight: 700;
font-size: 1rem;
color: var(--text);
letter-spacing: 0.03em;
}
.site-header .logo span {
color: var(--accent);
}
.site-header nav {
display: flex;
gap: 0.25rem;
margin-left: auto;
}
.site-header nav a {
padding: 0.3rem 0.75rem;
border-radius: var(--radius);
color: var(--text-muted);
font-size: 0.875rem;
transition: background var(--speed), color var(--speed);
}
.site-header nav a:hover,
.site-header nav a.active {
background: var(--bg-raised);
color: var(--text);
text-decoration: none;
}
+16
View File
@@ -0,0 +1,16 @@
/* Component – Hero section */
.hero {
padding: 2.5rem 0 2rem;
}
.hero h1 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.hero p {
color: var(--text-muted);
max-width: 540px;
}
+79
View File
@@ -0,0 +1,79 @@
/* Component – Tool panel (sidebar) */
.tool-panel {
width: 210px;
flex-shrink: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-section {
padding: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
}
.panel-section:last-child {
border-bottom: none;
}
.control-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.control-row label {
color: var(--text-muted);
min-width: 44px;
}
.tool-panel select {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
font-family: inherit;
transition: border-color var(--speed);
cursor: pointer;
}
.tool-panel select:focus {
outline: none;
border-color: var(--accent-dim);
}
.tool-panel input[type="checkbox"] {
accent-color: var(--accent);
cursor: pointer;
flex-shrink: 0;
}
.tool-panel input[type="number"] {
width: 52px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
font-family: inherit;
transition: border-color var(--speed);
}
.tool-panel input[type="number"]:focus {
outline: none;
border-color: var(--accent-dim);
}
.unit {
color: var(--text-muted);
font-size: 0.875rem;
}
@@ -0,0 +1,36 @@
/* Component – Preview area */
.tool-preview {
flex: 1;
position: relative;
min-height: 300px;
transition: border-color var(--speed);
}
.preview-scroll {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.preview-scroll canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
display: block;
max-width: none;
}
.preview-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--text-muted);
font-size: 0.875rem;
text-align: center;
white-space: nowrap;
user-select: none;
pointer-events: none;
}
@@ -0,0 +1,28 @@
/* Component – Background swatches */
.bg-swatches {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.bg-swatch {
width: 26px;
height: 26px;
border-radius: var(--radius-sm);
border: 2px solid transparent;
cursor: pointer;
padding: 0;
flex-shrink: 0;
overflow: hidden;
transition: border-color var(--speed);
}
.bg-swatch:hover {
border-color: var(--text-muted);
}
.bg-swatch.active,
.bg-swatch.active:hover {
border-color: var(--accent);
}
@@ -0,0 +1,28 @@
/* Component – Warnings panel */
.warnings-section {
border-color: #6b4a00;
}
.warning-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.warning-list li {
font-size: 0.775rem;
color: #e8a030;
line-height: 1.4;
padding-left: 1.1rem;
position: relative;
}
.warning-list li::before {
content: "!";
position: absolute;
left: 0;
font-weight: 700;
color: #e8a030;
}
+19
View File
@@ -0,0 +1,19 @@
/* Elements – Bare HTML */
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.6;
min-height: 100vh;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
+7
View File
@@ -0,0 +1,7 @@
/* Generic – Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
+15
View File
@@ -0,0 +1,15 @@
/* ITCSS entry point */
@import "settings.css";
@import "generic.css";
@import "elements.css";
@import "objects.css";
@import "components/header.css";
@import "components/hero.css";
@import "components/cards.css";
@import "components/panel.css";
@import "components/buttons.css";
@import "components/swatches.css";
@import "components/preview.css";
@import "components/file-info.css";
@import "components/warnings.css";
+59
View File
@@ -0,0 +1,59 @@
/* Objects – Layout patterns and shared abstractions */
/* Surface base – background, border, and radius shared across raised UI */
.tool-card,
.tool-panel,
.tool-preview,
.empty-state {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
}
/* Hover accent border – interactive surfaces that highlight on focus/drag */
.tool-card:hover,
.btn:hover:not(:disabled),
.load-area:hover,
.load-area.drag-over,
.tool-preview.drag-over {
border-color: var(--accent-dim);
}
/* Section label – small uppercase heading shared across page and panel contexts */
.section-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
/* Page container */
.page {
max-width: 1400px;
margin: 0 auto;
padding: var(--gap);
}
/* Top-level sections share uniform vertical spacing */
.section,
.tool-workspace {
margin-top: var(--gap);
}
.section > .section-label {
margin-bottom: 1rem;
}
/* Tool grid */
.tool-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
/* Tool workspace */
.tool-workspace {
display: flex;
gap: 1rem;
}
+16
View File
@@ -0,0 +1,16 @@
/* Settings – Design tokens */
:root {
--bg: #1a1a1e;
--bg-surface: #25252b;
--bg-raised: #2e2e36;
--border: #3a3a44;
--text: #e4e4ed;
--text-muted: #7a7a90;
--accent: #6e8efb;
--accent-dim: #3a4a8a;
--radius: 6px;
--radius-sm: 4px;
--gap: 1.5rem;
--speed: 0.15s;
}
+124
View File
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Texture Creator – Dusk Editor</title>
<link rel="stylesheet" href="/styles/main.css">
</head>
<body>
<header class="site-header">
<a href="/" class="logo" style="text-decoration:none;color:inherit">Dusk <span>Editor</span></a>
<nav>
<a href="/">Home</a>
<a href="/texture/" class="active">Texture Creator</a>
</nav>
</header>
<main class="page">
<div class="hero">
<h1>Texture Creator</h1>
<p>Load an image or existing .dtf file, then export as Dusk Texture Format (.dtf) or PNG.</p>
</div>
<div class="tool-workspace">
<!-- ─── Sidebar ────────────────────────────────────────────────────── -->
<aside class="tool-panel">
<section class="panel-section">
<div class="section-label">Load</div>
<label class="load-area" id="load-label">
<input type="file" id="file-input" accept=".png,.jpg,.jpeg,.gif,.bmp,.webp,.dtf" style="display:none">
Click or drop a file<br>
<small>PNG · JPG · GIF · BMP · WebP · DTF</small>
</label>
<div class="file-name" id="file-name">No file loaded</div>
</section>
<section class="panel-section">
<div class="section-label">Format</div>
<select id="format-select">
<option value="1">Alpha (0x01)</option>
<option value="3">RGB (0x03)</option>
<option value="4" selected>RGBA (0x04)</option>
</select>
<div class="control-row" id="red-as-alpha-row" hidden>
<input type="checkbox" id="red-as-alpha">
<label for="red-as-alpha">Red channel as alpha</label>
</div>
</section>
<section class="panel-section">
<div class="section-label">Preview</div>
<div class="control-row">
<label for="scale-input">Scale</label>
<input type="number" id="scale-input" min="1" max="10" value="1">
<span class="unit">×</span>
</div>
<div class="section-label">Background</div>
<div class="bg-swatches" id="bg-swatches">
<!-- Transparent grid -->
<button class="bg-swatch active" data-bg="grid" title="Transparent (grid)">
<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg">
<rect width="13" height="13" fill="#c8c8c8"/>
<rect x="13" width="13" height="13" fill="#fff"/>
<rect y="13" width="13" height="13" fill="#fff"/>
<rect x="13" y="13" width="13" height="13" fill="#c8c8c8"/>
</svg>
</button>
<button class="bg-swatch" data-bg="#ffffff" title="White" style="background:#ffffff"></button>
<button class="bg-swatch" data-bg="#000000" title="Black" style="background:#000000"></button>
<button class="bg-swatch" data-bg="#ff00ff" title="Magenta" style="background:#ff00ff"></button>
<button class="bg-swatch" data-bg="#00ff00" title="Green" style="background:#00ff00"></button>
<button class="bg-swatch" data-bg="#ff0000" title="Red" style="background:#ff0000"></button>
<button class="bg-swatch" data-bg="#0000ff" title="Blue" style="background:#0000ff"></button>
<button class="bg-swatch" data-bg="#ffff00" title="Yellow" style="background:#ffff00"></button>
</div>
</section>
<section class="panel-section" id="info-section" hidden>
<div class="section-label">Info</div>
<table class="info-table">
<tr><td>File</td><td id="info-filename">—</td></tr>
<tr><td>Size</td><td id="info-size">—</td></tr>
<tr><td>Format</td><td id="info-format">—</td></tr>
<tr><td>DTF</td><td id="info-dtf-size">—</td></tr>
</table>
</section>
<section class="panel-section warnings-section" id="warnings-section" hidden>
<div class="section-label">Warnings</div>
<ul class="warning-list" id="warning-list"></ul>
</section>
<section class="panel-section">
<div class="section-label">Export</div>
<button class="btn btn-primary" id="btn-dtf" disabled>Download .dtf</button>
<button class="btn" id="btn-png" disabled>Download .png</button>
</section>
</aside>
<!-- ─── Preview ───────────────────────────────────────────────────── -->
<div class="tool-preview" id="preview-area">
<div class="preview-empty" id="preview-empty">Load an image or .dtf file to get started</div>
<div class="preview-scroll" id="preview-scroll" hidden>
<canvas id="canvas"></canvas>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/browser.js"></script>
<script src="/common/dtf.js"></script>
<script src="/common/png.js"></script>
<script src="/texture/texture.js"></script>
</body>
</html>
+278
View File
@@ -0,0 +1,278 @@
"use strict";
// ─── State ───────────────────────────────────────────────────────────────────
const state = {
pixels: null, // Uint8ClampedArray RGBA at original resolution
width: 0,
height: 0,
scale: 1,
bg: "grid",
format: DTF.FORMAT_RGBA, // output DTF format
redAsAlpha: false, // Alpha format: use red channel instead of alpha
filename: "texture", // basename without extension
};
// ─── DOM refs ────────────────────────────────────────────────────────────────
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const fileInput = document.getElementById("file-input");
const loadLabel = document.getElementById("load-label");
const scaleInput = document.getElementById("scale-input");
const bgSwatches = document.getElementById("bg-swatches");
const previewArea = document.getElementById("preview-area");
const previewEmpty = document.getElementById("preview-empty");
const previewScroll = document.getElementById("preview-scroll");
const infoSection = document.getElementById("info-section");
const fileNameEl = document.getElementById("file-name");
const infoFilename = document.getElementById("info-filename");
const infoSize = document.getElementById("info-size");
const infoFormat = document.getElementById("info-format");
const btnDtf = document.getElementById("btn-dtf");
const btnPng = document.getElementById("btn-png");
const formatSelect = document.getElementById("format-select");
const infoDtfSize = document.getElementById("info-dtf-size");
const warningsSection = document.getElementById("warnings-section");
const warningList = document.getElementById("warning-list");
const redAsAlphaRow = document.getElementById("red-as-alpha-row");
const redAsAlphaCheck = document.getElementById("red-as-alpha");
// ─── Rendering ───────────────────────────────────────────────────────────────
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) {
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));
}
}
}
function render() {
if (!state.pixels) return;
const { pixels, width, height, scale, bg, format } = state;
const cw = width * scale;
const ch = height * scale;
canvas.width = cw;
canvas.height = ch;
// 1. Background
if (bg === "grid") {
drawCheckerboard(cw, ch);
} else {
ctx.fillStyle = bg;
ctx.fillRect(0, 0, cw, ch);
}
// 2. Composite image on top at scaled size (pixelated)
const src = Object.assign(document.createElement("canvas"), { width, height });
src.getContext("2d").putImageData(DTF.toImageData(width, height, pixels, format, state.redAsAlpha), 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(src, 0, 0, cw, ch);
}
// ─── Loading ─────────────────────────────────────────────────────────────────
function loadStandardImage(file) {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const offscreen = Object.assign(document.createElement("canvas"), {
width: img.naturalWidth,
height: img.naturalHeight,
});
offscreen.getContext("2d").drawImage(img, 0, 0);
const imageData = offscreen.getContext("2d").getImageData(0, 0, img.naturalWidth, img.naturalHeight);
applyImageData(
img.naturalWidth,
img.naturalHeight,
imageData.data,
file.name,
file.type || "image",
);
URL.revokeObjectURL(url);
};
img.onerror = () => {
showError("Failed to load image. The file may be corrupt or unsupported.");
URL.revokeObjectURL(url);
};
img.src = url;
}
function loadDTF(file) {
const reader = new FileReader();
reader.onload = e => {
try {
const { width, height, format, data } = DTF.decode(e.target.result);
const label = format === DTF.FORMAT_ALPHA ? "DTF (Alpha)"
: format === DTF.FORMAT_RGB ? "DTF (RGB)"
: "DTF (RGBA)";
applyImageData(width, height, data, file.name, label, format);
} catch (err) {
showError(`Failed to load DTF: ${err.message}`);
}
};
reader.readAsArrayBuffer(file);
}
function updateWarnings() {
const warnings = [];
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})`);
const bytes = DTF.HEADER_SIZE + width * height * DTF.BPP[state.format];
if (bytes > 256 * 1024) {
warnings.push(`Output exceeds 256 KB (${(bytes / 1024).toFixed(1)} KB)`);
}
}
warningList.replaceChildren(
...warnings.map(msg => Object.assign(document.createElement("li"), { textContent: msg })),
);
warningsSection.hidden = warnings.length === 0;
}
function updateDtfSize() {
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();
}
function applyImageData(width, height, data, filename, formatLabel, format) {
state.pixels = new Uint8ClampedArray(data); // defensive copy
state.width = width;
state.height = height;
state.filename = filename.replace(/\.[^/.]+$/, "");
// Sync format selector when loading an existing DTF
if (format !== undefined) {
state.format = format;
formatSelect.value = format;
}
// Sidebar info
fileNameEl.textContent = filename;
infoFilename.textContent = filename;
infoSize.textContent = `${width} × ${height}`;
infoFormat.textContent = formatLabel;
infoSection.hidden = false;
updateDtfSize();
// Show canvas
previewEmpty.hidden = true;
previewScroll.hidden = false;
// Enable export
btnDtf.disabled = false;
btnPng.disabled = false;
render();
}
function handleFile(file) {
if (!file) return;
if (file.name.toLowerCase().endsWith(".dtf")) {
loadDTF(file);
} else {
loadStandardImage(file);
}
}
function showError(msg) {
alert(msg);
}
// ─── Export ───────────────────────────────────────────────────────────────────
function exportDTF() {
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);
const a = Object.assign(document.createElement("a"), {
href: url,
download: `${state.filename}.dtf`,
});
a.click();
URL.revokeObjectURL(url);
}
function exportPNG() {
if (!state.pixels) return;
DuskPNG.download(`${state.filename}.png`, state.width, state.height, state.pixels);
}
// ─── Event listeners ─────────────────────────────────────────────────────────
fileInput.addEventListener("change", e => handleFile(e.target.files[0]));
scaleInput.addEventListener("input", () => {
const v = Math.max(1, Math.min(10, parseInt(scaleInput.value, 10) || 1));
scaleInput.value = v;
state.scale = v;
render();
});
bgSwatches.addEventListener("click", e => {
const btn = e.target.closest(".bg-swatch");
if (!btn) return;
bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
state.bg = btn.dataset.bg;
render();
});
btnDtf.addEventListener("click", exportDTF);
btnPng.addEventListener("click", exportPNG);
formatSelect.addEventListener("change", () => {
state.format = parseInt(formatSelect.value, 10);
redAsAlphaRow.hidden = state.format !== DTF.FORMAT_ALPHA;
updateDtfSize();
render();
});
redAsAlphaCheck.addEventListener("change", () => {
state.redAsAlpha = redAsAlphaCheck.checked;
render();
});
// Drag-and-drop on the load label
function setupDrop(target) {
target.addEventListener("dragover", e => {
e.preventDefault();
loadLabel.classList.add("drag-over");
previewArea.classList.add("drag-over");
});
target.addEventListener("dragleave", () => {
loadLabel.classList.remove("drag-over");
previewArea.classList.remove("drag-over");
});
target.addEventListener("drop", e => {
e.preventDefault();
loadLabel.classList.remove("drag-over");
previewArea.classList.remove("drag-over");
handleFile(e.dataTransfer.files[0]);
});
}
setupDrop(loadLabel);
setupDrop(previewArea);
-15
View File
@@ -1,15 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
function(dusk_env_to_h INPUT_PATH OUTPUT_NAME_RELATIVE)
set(DUSK_DEFS_TARGET_NAME "DUSK_DEFS_${OUTPUT_NAME_RELATIVE}")
dusk_run_python(
${DUSK_DEFS_TARGET_NAME}
tools.env_to_h
--env "${CMAKE_CURRENT_LIST_DIR}/${INPUT_PATH}"
--output ${DUSK_GENERATED_HEADERS_DIR}/${OUTPUT_NAME_RELATIVE}
)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} ${DUSK_DEFS_TARGET_NAME})
endfunction()
-46
View File
@@ -1,46 +0,0 @@
import argparse
import os
from dotenv import load_dotenv, dotenv_values
parser = argparse.ArgumentParser(description="Convert .env to .h defines")
parser.add_argument("--env", required=True, help="Path to .env file")
parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args()
# Load .env file
load_dotenv(dotenv_path=args.env)
fileDefs = dotenv_values(dotenv_path=args.env)
outHeader = ""
outHeader += "#include \"dusk.h\"\n\n"
for key, value in fileDefs.items():
# Determine type and print out appropriate C type define.
# Integer
try:
asInt = int(value)
outHeader += f"#define {key} {asInt}\n"
continue
except:
pass
# Float
try:
asFloat = float(value)
outHeader += f"#define {key} {asFloat}f\n"
continue
except:
pass
# Boolean
if value.lower() in ['true', 'false']:
asBool = '1' if value.lower() == 'true' else '0'
outHeader += f"#define {key} {asBool}\n"
continue
# String
outHeader += f'#define {key} "{value}"\n'
# Write to output file
with open(args.output, 'w') as outFile:
outFile.write(outHeader)
+43 -45
View File
@@ -1,60 +1,58 @@
import argparse
import os
import csv
import os
parser = argparse.ArgumentParser(description="Input CSV to .h defines")
parser.add_argument("--csv", required=True, help="Path to Input CSV file")
parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args()
def csvIdToEnumName(inputId):
return "INPUT_ACTION_" + inputId.upper()
def id_enum(name):
return "INPUT_ACTION_" + name.upper()
# Load up CSV file.
outHeader = "#pragma once\n"
outHeader += '#include "dusk.h"\n\n'
with open(args.csv, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
# CSV must have id column.
# Load CSV
input_ids = []
with open(args.csv, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if "id" not in reader.fieldnames:
raise Exception("CSV file must have 'id' column")
# For each ID
inputIds = []
inputIdValues = {}
raise ValueError("CSV must have an 'id' column")
for row in reader:
inputId = row["id"]
if inputId not in inputIds:
inputIds.append(inputId)
input_id = row["id"]
if input_id not in input_ids:
input_ids.append(input_id)
# For each ID, create enum entry.
count = 0
outHeader += "typedef enum {\n"
outHeader += f" INPUT_ACTION_NULL = 0x{count:x},\n\n"
count += 1
for inputId in inputIds:
inputIdValues[inputId] = count
outHeader += f" {csvIdToEnumName(inputId)} = 0x{count:x},\n"
count += 1
outHeader += f"\n INPUT_ACTION_COUNT = 0x{count:x}\n"
outHeader += "} inputaction_t;\n\n"
# Assign enum values
id_values = {input_id: i + 1 for i, input_id in enumerate(input_ids)}
# Write IDs to char array.
outHeader += f"static const char_t* INPUT_ACTION_IDS[] = {{\n"
for inputId in inputIds:
outHeader += f" [{csvIdToEnumName(inputId)}] = \"{inputId}\",\n"
outHeader += f"}};\n\n"
# Build output
out = [
"#pragma once",
'#include "dusk.h"',
"",
"typedef enum {",
" INPUT_ACTION_NULL = 0x0,",
"",
]
for input_id in input_ids:
out.append(f" {id_enum(input_id)} = 0x{id_values[input_id]:x},")
out += [
"",
f" INPUT_ACTION_COUNT = 0x{len(input_ids) + 1:x}",
"} inputaction_t;",
"",
"static const char_t *INPUT_ACTION_IDS[] = {",
]
for input_id in input_ids:
out.append(f" [{id_enum(input_id)}] = \"{input_id}\",")
out += [
"};",
"",
"static const char_t *INPUT_ACTION_SCRIPT =",
]
for input_id in input_ids:
out.append(f" \"{id_enum(input_id)} = {id_values[input_id]}\\n\"")
out += [";", ""]
# Lua Script
outHeader += f"static const char_t *INPUT_ACTION_SCRIPT = \n"
for inputId in inputIds:
# Reference the enum
outHeader += f" \"{csvIdToEnumName(inputId)} = {inputIdValues[inputId]}\\n\"\n"
outHeader += f";\n\n"
# Write to output file.
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as outFile:
outFile.write(outHeader)
with open(args.output, "w", encoding="utf-8") as f:
f.write("\n".join(out))
+83 -73
View File
@@ -1,94 +1,104 @@
import argparse
import os
import csv
import os
parser = argparse.ArgumentParser(description="Item CSV to .h defines")
parser.add_argument("--csv", required=True, help="Path to item CSV file")
parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args()
def csvIdToEnumName(itemId):
return "ITEM_ID_" + itemId.upper()
def type_enum(name):
return "ITEM_TYPE_" + name.upper()
itemIds = []
itemTypes = []
itemRowById = {}
def id_enum(name):
return "ITEM_ID_" + name.upper()
with open(args.csv, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
# Load CSV
item_ids = []
item_types = []
rows = {}
# CSV must have id and type columns.
with open(args.csv, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if "id" not in reader.fieldnames or "type" not in reader.fieldnames:
raise Exception("CSV file must have 'id' and 'type' columns")
raise ValueError("CSV must have 'id' and 'type' columns")
for row in reader:
itemId = row["id"]
itemType = row["type"]
item_id, item_type = row["id"], row["type"]
if item_id not in item_ids:
item_ids.append(item_id)
if item_type not in item_types:
item_types.append(item_type)
rows[item_id] = row
if itemId not in itemIds:
itemIds.append(itemId)
if itemType not in itemTypes:
itemTypes.append(itemType)
itemRowById[itemId] = row
# Now Prep output
outHeader = "#pragma once\n"
outHeader += '#include "dusk.h"\n\n'
itemTypeValues = {}
itemIdValues = {}
# Assign enum values — types and IDs share a single counter so values never collide
count = 0
type_values = {}
id_values = {}
# Create enum for types and ids, include null and count.
outHeader += "typedef enum {\n"
outHeader += f" ITEM_TYPE_NULL = {count},\n"
count += 1 # 0 = NULL
for t in item_types:
type_values[t] = count
count += 1
for itemType in itemTypes:
itemTypeValues[itemType] = count
outHeader += f" {csvIdToEnumName(itemType)} = {count},\n"
# ITEM_TYPE_COUNT = count; item IDs continue from here
type_count = count
count += 1 # type_count = ITEM_ID_NULL
for i in item_ids:
id_values[i] = count
count += 1
outHeader += f" ITEM_TYPE_COUNT = {count}\n"
outHeader += "} itemtype_t;\n\n"
id_count = count
outHeader += "typedef enum {\n"
outHeader += f" ITEM_ID_NULL = {count},\n"
for itemId in itemIds:
itemIdValues[itemId] = count
outHeader += f" {csvIdToEnumName(itemId)} = {count},\n"
count += 1
outHeader += f" ITEM_ID_COUNT = {count}\n"
outHeader += "} itemid_t;\n\n"
# Build output
out = [
"#pragma once",
'#include "dusk.h"',
"",
"typedef enum {",
" ITEM_TYPE_NULL = 0,",
]
for t in item_types:
out.append(f" {type_enum(t)} = {type_values[t]},")
out += [
f" ITEM_TYPE_COUNT = {type_count}",
"} itemtype_t;",
"",
"typedef enum {",
f" ITEM_ID_NULL = {type_count},",
]
for i in item_ids:
out.append(f" {id_enum(i)} = {id_values[i]},")
out += [
f" ITEM_ID_COUNT = {id_count}",
"} itemid_t;",
"",
"typedef struct {",
" itemid_t id;",
" itemtype_t type;",
" const char_t *name;",
"} item_t;",
"",
"static const item_t ITEMS[] = {",
]
for i in item_ids:
row = rows[i]
out += [
f" [{id_enum(i)}] = {{",
f" .id = {id_enum(i)},",
f" .type = {type_enum(row['type'])},",
f" .name = \"{i}\",",
" },",
]
out += [
"};",
"",
"static const char_t *ITEM_SCRIPT =",
]
for i in item_ids:
out.append(f" \"{id_enum(i)} = {id_values[i]}\\n\"")
for t in item_types:
out.append(f" \"{type_enum(t)} = {type_values[t]}\\n\"")
out += [";", ""]
# Create struct for item data.
outHeader += "typedef struct {\n"
outHeader += " itemid_t id;\n"
outHeader += " itemtype_t type;\n"
outHeader += " const char_t *name;\n"
outHeader += "} item_t;\n\n"
# Create array of item data.
outHeader += f"static const item_t ITEMS[] = {{\n"
for itemId in itemIds:
outHeader += f" [{csvIdToEnumName(itemId)}] = {{\n"
outHeader += f" .id = {csvIdToEnumName(itemId)},\n"
itemType = itemRowById[itemId]["type"]
outHeader += f" .type = {csvIdToEnumName(itemType)},\n"
itemName = itemRowById[itemId]["id"]
outHeader += f" .name = \"{itemName}\",\n"
outHeader += f" }},\n"
outHeader += f"}};\n\n"
# Create lua script defining items.
outHeader += f"static const char_t *ITEM_SCRIPT = \n"
for itemId in itemIds:
outHeader += f" \"{csvIdToEnumName(itemId)} = {itemIdValues[itemId]}\\n\"\n"
for itemType in itemTypes:
outHeader += f" \"{csvIdToEnumName(itemType)} = {itemTypeValues[itemType]}\\n\"\n"
outHeader += f";\n\n"
# Write to output file.
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as outFile:
outFile.write(outHeader)
with open(args.output, "w", encoding="utf-8") as f:
f.write("\n".join(out))
-687
View File
@@ -1,687 +0,0 @@
<!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';
// Convert from little endian
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)
// Color count (1 byte)
const colorCountBytes = new Uint8Array(1);
colorCountBytes[0] = palette.length;
// 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
// Dimensions
const widthBytes = new Uint32Array([ imageWidth ]);
const heightBytes = new Uint32Array([ imageHeight ]);
// 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>
-5
View File
@@ -3,11 +3,6 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
function(dusk_run_python CMAKE_TARGET_NAME PYTHON_MODULE)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
import argparse
import functools
import http.server
import os
EDITOR_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor")
parser = argparse.ArgumentParser(description="Dusk editor tools server")
parser.add_argument("--host", default="localhost", help="Host to bind to")
parser.add_argument("--port", type=int, default=3000, help="Port to listen on")
args = parser.parse_args()
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=EDITOR_DIR)
with http.server.HTTPServer((args.host, args.port), handler) as server:
print(f"Dusk editor tools: http://{args.host}:{args.port}/")
server.serve_forever()
+37 -36
View File
@@ -1,48 +1,49 @@
import argparse
import os
import csv
import os
parser = argparse.ArgumentParser(description="Story CSV to .h defines")
parser.add_argument("--csv", required=True, help="Path to story CSV file")
parser.add_argument("--header-file", required=True, help="Path to output .h file")
parser.add_argument("--output", required=True, help="Path to output .h file")
args = parser.parse_args()
def idToEnum(id):
return "STORY_FLAG_" + id.upper().replace(" ", "_")
def flag_enum(name):
return "STORY_FLAG_" + name.upper().replace(" ", "_")
# Load up CSV file.
outHeader = "#pragma once\n"
outHeader += '#include "story/storyflagdefs.h"\n\n'
with open(args.csv, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
# CSV must have id column
# Load flags
flags = []
with open(args.csv, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if "id" not in reader.fieldnames:
raise Exception("CSV file must have 'id' column")
# Generate enum
outHeader += "typedef enum {\n"
outHeader += " STORY_FLAG_NULL,\n\n"
raise ValueError("CSV must have an 'id' column")
for row in reader:
id = idToEnum(row["id"].strip())
outHeader += f" {id},\n"
outHeader += "\n STORY_FLAG_COUNT\n"
outHeader += "} storyflag_t;\n\n"
flags.append({
"id": row["id"].strip(),
"initial": (row.get("initial") or "0").strip(),
})
# Generate flag values
csvfile.seek(0)
reader = csv.DictReader(csvfile)
# Build output
out = [
"#pragma once",
'#include "story/storyflagdefs.h"',
"",
"typedef enum {",
" STORY_FLAG_NULL,",
"",
]
for flag in flags:
out.append(f" {flag_enum(flag['id'])},")
out += [
"",
" STORY_FLAG_COUNT",
"} storyflag_t;",
"",
"static storyflagvalue_t STORY_FLAG_VALUES[STORY_FLAG_COUNT] = {",
]
for flag in flags:
out.append(f" [{flag_enum(flag['id'])}] = {flag['initial']},")
out += ["};", ""]
outHeader += "static storyflagvalue_t STORY_FLAG_VALUES[STORY_FLAG_COUNT] = {\n"
for row in reader:
id = idToEnum(row["id"].strip())
initial = row.get("initial", "0").strip() or "0"
outHeader += f" [{id}] = {initial},\n"
outHeader += "};\n"
os.makedirs(os.path.dirname(args.header_file), exist_ok=True)
# Write header
with open(args.header_file, "w", encoding="utf-8") as headerFile:
headerFile.write(outHeader)
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
f.write("\n".join(out))
-417
View File
@@ -1,417 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dusk Tools / Texture Creator</title>
<style type="text/css">
* {
box-sizing: border-box;
}
body {
font-size: 16px;
}
canvas {
image-rendering: pixelated;
}
</style>
</head>
<body>
<h1>Dusk Texture Creator</h1>
<p>
Creates texture files. This will not create palletized textures, use the
palette-indexer.html tool for that. This will instead work for all other
kinds of textures.
</p>
<div>
<div>
<input type="file" data-file-input />
</div>
</div>
<div>
<h2>Settings</h2>
<div>
Texture Format:
<div>
<label>
RGBA
<input type="radio" name="texture-type" value="rgba" />
</label><br />
<label>
RGB
<input type="radio" name="texture-type" value="rgb" />
</label><br />
<label>
RGB565
<input type="radio" name="texture-type" value="rgb565" checked />
</label><br />
<label>
RGB5A3 (Dolphin Only)
<input type="radio" name="texture-type" value="rgb5a3" />
</label><br />
</div>
</div>
<div>
<label for="pad-power-of-two">
Pad to power of two
<input type="checkbox" id="pad-power-of-two" name="pad-power-of-two" checked />
</label>
</div>
</div>
<div>
<h2>Preview</h2>
<div>
<label>
Preview Scale:
<input type="number" value="2" data-preview-scale min="1" step="1" />
</label>
</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>
<canvas data-output-preview style="border:1px solid black;"></canvas>
</div>
<div>
<textarea data-output-information readonly rows="15" style="width: 500px;"></textarea>
</div>
<div>
<button data-download>Download Texture</button>
</div>
</div>
</body>
<script type="text/javascript">
const elFileInput = document.querySelector('[data-file-input]');
const elFileError = document.querySelector('[data-file-error]');
const elPreviewScale = document.querySelector('[data-preview-scale]');
const elOutputPreview = document.querySelector('[data-output-preview]');
const elOutputInformation = document.querySelector('[data-output-information]');
const elTextureType = document.querySelectorAll('input[name="texture-type"]');
const elPadPowerOfTwo = document.querySelector('#pad-power-of-two');
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 image;
let imageName;
let textureData;
let rawData;
let width, height, paddedWidth, paddedHeight;
// Methods
const nextPowerOfTwo = n => {
if(n <= 0) return 1;
return 2 ** Math.ceil(Math.log2(n));
}
const updatePreview = () => {
if(!image) return;
console.log('Updating preview with image:', image);
const scale = parseInt(elPreviewScale.value) || 1;
const padToPowerOfTwo = elPadPowerOfTwo.checked;
const textureType = Array.from(elTextureType).find(r => r.checked)?.value || 'rgba';
width = image.width;
height = image.height;
let scaledWidth = width * scale;
let scaledHeight = height * scale;
paddedWidth = padToPowerOfTwo ? nextPowerOfTwo(width) : width;
paddedHeight = padToPowerOfTwo ? nextPowerOfTwo(height) : height;
let paddedScaledWidth = paddedWidth * scale;
let paddedScaledHeight = paddedHeight * scale;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = paddedWidth;
tempCanvas.height = paddedHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.imageSmoothingEnabled = false;
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempCtx.drawImage(image, 0, 0, width, height);
const pixels = tempCtx.getImageData(0, 0, paddedWidth, paddedHeight);
let sizeBytes;
let strTextureType = Array.from(elTextureType).find(r => r.checked)?.value || 'rgba';
strTextureType = strTextureType.toLowerCase();
rawData = new Uint8Array(paddedWidth * paddedHeight * 4);
if(strTextureType === 'rgba') {
textureData = new Uint8Array(pixels.data.buffer);
rawData = new Uint8Array(pixels.data.buffer);
sizeBytes = paddedWidth * paddedHeight * 4;
} else if(strTextureType === 'rgb') {
sizeBytes = paddedWidth * paddedHeight * 3;
textureData = new Uint8Array(sizeBytes);
for(let i = 0, j = 0; i < textureData.length; i += 4, j += 3) {
textureData[j] = pixels.data[i]; // R
textureData[j + 1] = pixels.data[i + 1]; // G
textureData[j + 2] = pixels.data[i + 2]; // B
rawData[i] = pixels.data[i]; // R
rawData[i + 1] = pixels.data[i + 1]; // G
rawData[i + 2] = pixels.data[i + 2]; // B
rawData[i + 3] = 255;
}
} else if(strTextureType === 'rgb565') {
sizeBytes = paddedWidth * paddedHeight * 2;
textureData = new Uint8Array(sizeBytes);
let j = 0;
for (let i = 0; i < pixels.data.length; i += 4) {
const r = pixels.data[i];
const g = pixels.data[i + 1];
const b = pixels.data[i + 2];
const value =
((r & 0xf8) << 8) |
((g & 0xfc) << 3) |
(b >> 3);
textureData[j++] = (value >> 8) & 0xff; // high byte
textureData[j++] = value & 0xff; // low byte
}
// Now convert back to RGBA for preview
for(let i = 0, j = 0; i < textureData.length; i += 2, j += 4) {
const value = (textureData[i] << 8) | textureData[i + 1];
const r = (value >> 11) & 0x1f;
const g = (value >> 5) & 0x3f;
const b = value & 0x1f;
rawData[j] = (r << 3) | (r >> 2); // R
rawData[j + 1] = (g << 2) | (g >> 4); // G
rawData[j + 2] = (b << 3) | (b >> 2); // B
rawData[j + 3] = 255; // A
}
} else if(strTextureType === 'rgb5a3') {
sizeBytes = paddedWidth * paddedHeight * 2;
textureData = new Uint8Array(sizeBytes);
let j = 0;
for (let i = 0; i < pixels.data.length; i += 4) {
const r8 = pixels.data[i];
const g8 = pixels.data[i + 1];
const b8 = pixels.data[i + 2];
const a8 = pixels.data[i + 3];
let value;
// Opaque: 1RRRRRGGGGGBBBBB
if (a8 >= 224) {
const r5 = r8 >> 3;
const g5 = g8 >> 3;
const b5 = b8 >> 3;
value =
0x8000 |
(r5 << 10) |
(g5 << 5) |
b5;
} else {
// Transparent/translucent: 0AAARRRRGGGGBBBB
const a3 = a8 >> 5;
const r4 = r8 >> 4;
const g4 = g8 >> 4;
const b4 = b8 >> 4;
value =
(a3 << 12) |
(r4 << 8) |
(g4 << 4) |
b4;
}
textureData[j++] = (value >> 8) & 0xff; // high byte
textureData[j++] = value & 0xff; // low byte
}
// Convert back to RGBA for preview
for (let i = 0, j = 0; i < textureData.length; i += 2, j += 4) {
const value = (textureData[i] << 8) | textureData[i + 1];
if (value & 0x8000) {
// 1RRRRRGGGGGBBBBB
const r5 = (value >> 10) & 0x1f;
const g5 = (value >> 5) & 0x1f;
const b5 = value & 0x1f;
rawData[j] = (r5 << 3) | (r5 >> 2);
rawData[j + 1] = (g5 << 3) | (g5 >> 2);
rawData[j + 2] = (b5 << 3) | (b5 >> 2);
rawData[j + 3] = 255;
} else {
// 0AAARRRRGGGGBBBB
const a3 = (value >> 12) & 0x7;
const r4 = (value >> 8) & 0xf;
const g4 = (value >> 4) & 0xf;
const b4 = value & 0xf;
rawData[j] = (r4 << 4) | r4;
rawData[j + 1] = (g4 << 4) | g4;
rawData[j + 2] = (b4 << 4) | b4;
rawData[j + 3] = (a3 << 5) | (a3 << 2) | (a3 >> 1);
}
}
} else {
return alert('Unsupported texture type selected.');
}
// Write out pixels.
const imageData = new ImageData(new Uint8ClampedArray(rawData.buffer), paddedWidth, paddedHeight);
tempCanvas.width = paddedWidth;
tempCanvas.height = paddedHeight;
const tempCtx2 = tempCanvas.getContext('2d');
tempCtx2.imageSmoothingEnabled = false;
tempCtx2.putImageData(imageData, 0, 0);
elOutputPreview.width = paddedScaledWidth;
elOutputPreview.height = paddedScaledHeight;
const ctx = elOutputPreview.getContext('2d');
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height);
ctx.drawImage(tempCanvas, 0, 0, paddedScaledWidth, paddedScaledHeight);
// Output information
elOutputInformation.value = `Original Size: ${width}x${height}\n`;
elOutputInformation.value += `New Size: ${paddedWidth}x${paddedHeight}\n`;
elOutputInformation.value += `Texture Format: ${textureType}\n`;
elOutputInformation.value += `Size: ${sizeBytes} bytes / (${(sizeBytes / 1024).toFixed(2)} KB)\n`;
}
const onFile = async file => {
elFileInput.disabled = true;
console.log('Selected file:', file);
imageName = file.name;
// Load as image
const img = new Image();
img.onload = () => {
image = img;
elFileInput.disabled = false;
updatePreview();
};
img.onerror = () => {
alert('Failed to load image. Please select a valid image file.');
elFileInput.disabled = false;
};
img.src = URL.createObjectURL(file);
}
// Listeners
elFileInput.addEventListener('change', event => {
if(!event.target.files || event.target.files.length <= 0) {
return alert('No file selected.');
}
const file = event.target.files[0];
if(!file.type.startsWith('image/')) {
return alert('Selected file is not an image.');
}
onFile(file);
});
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';
});
elPreviewScale.addEventListener('input', () => {
updatePreview();
});
elPadPowerOfTwo.addEventListener('change', () => {
updatePreview();
});
elTextureType.forEach(radio => {
radio.addEventListener('change', () => {
updatePreview();
});
})
btnDownload.addEventListener('click', () => {
if(!image) {
return alert('Please select an image file before downloading.');
}
// DTX, then texture type, then width, then height, then raw data
const dtfHeader = new TextEncoder().encode('DTX');
const versionHeader = new Uint8Array([1]); // Version 1
let strTextureType = Array.from(elTextureType).find(r => r.checked)?.value || 'rgba';
strTextureType = strTextureType.toLowerCase();
let typeHeader;
if(strTextureType === 'rgba') {
typeHeader = new Uint8Array([0]);
} else if(strTextureType === 'rgb') {
typeHeader = new Uint8Array([1]);
} else if(strTextureType === 'rgb565') {
typeHeader = new Uint8Array([2]);
} else if(strTextureType === 'rgb5a3') {
typeHeader = new Uint8Array([3]);
} else {
return alert('Unsupported texture type selected.');
}
const widthBytes = new Uint32Array([ paddedWidth ]);
const heightBytes = new Uint32Array([ paddedHeight ]);
const blob = new Blob([dtfHeader, versionHeader, typeHeader, widthBytes, heightBytes, textureData], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${imageName.replace(/\.[^/.]+$/, "")}.dtx`;
a.click();
URL.revokeObjectURL(url);
});
btnBackgroundCheckerboard.click();
</script>
</html>
-235
View File
@@ -1,235 +0,0 @@
<!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.');
}
// Sort images by name to ensure consistent output
images = Object.fromEntries(Object.entries(images).sort(([nameA], [nameB]) => nameA.localeCompare(nameB)));
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 {
if(allImagesShareWidth) {
outputWidth = nextPowerOfTwo(firstWidth);
outputHeight = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.height, 0));
} else if(allImagesShareHeight) {
outputHeight = nextPowerOfTwo(firstHeight);
outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0));
} else {
onBadImages('All images must share the same width or height to be joined together.');
return;
}
}
// 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
View File
@@ -1,391 +0,0 @@
<!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 Slicer</title>
<style type="text/css">
* {
box-sizing: border-box;
}
body {
font-size: 16px;
}
canvas {
image-rendering: pixelated;
}
</style>
</head>
<body>
<h1>Dusk Tile Slicer</h1>
<p>
Tool allows you to take a single image and slice it into individual tiles.
This should be done as the first step of optimizing tilesets for your
dusk game.
</p>
<div>
<div>
<input type="file" data-file-input />
</div>
<p data-file-error style="color:red;display:none;"></p>
<h2>Input Preview</h2>
<canvas data-input-preview style="border:1px solid black;"></canvas>
</div>
<div>
<h2>Settings</h2>
<div>
<label>
Tile Width:
<input type="number" value="8" data-tile-width />
</label>
</div>
<div>
<label>
Tile Height:
<input type="number" value="8" data-tile-height />
</label>
</div>
<div>
<label>
Padding:
<input type="number" value="0" data-tile-padding />
</label>
</div>
<div>
<label>
Margin:
<input type="number" value="0" data-tile-margin />
</label>
</div>
<div>
<label>
Remove Duplicate Tiles:
<input type="checkbox" data-remove-duplicates checked />
</label>
</div>
<div>
<label>
Remove Empty Tiles:
<input type="checkbox" data-remove-empty checked />
</label>
</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>Sliced Preview</h2>
<div data-output-preview></div>
<div data-output-hover></div>
<textarea readonly data-output-information rows="10" style="width:500px"></textarea>
<div>
<button data-download-archive>Download All Tiles</button>
</div>
</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 elOutputPreview = document.querySelector('[data-output-preview]');
const elTileWidth = document.querySelector('[data-tile-width]');
const elTileHeight = document.querySelector('[data-tile-height]');
const elTilePadding = document.querySelector('[data-tile-padding]');
const elTileMargin = document.querySelector('[data-tile-margin]');
const elRemoveDuplicates = document.querySelector('[data-remove-duplicates]');
const elRemoveEmpty = document.querySelector('[data-remove-empty]');
const elOutputInformation = document.querySelector('[data-output-information]');
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 btnDownloadTiles = document.querySelector('[data-download-archive]');
let image = null;
let tileCanvases = [];
// Values updated during processing
let cols, rows;
let tileWidth, tileHeight;
let tilePadding, tileMargin;
let emptyTiles, duplicateTiles;
let hoveredTileX = -1, hoveredTileY = -1;
let tileStatus = {};
const updateOutputInformation = () => {
if(!image) {
elOutputInformation.value = 'No image loaded';
return;
}
elOutputInformation.value = [
`Input Tile Count: ${cols * rows}`,
`Output Tile Count: ${tileCanvases.length}`,
elRemoveEmpty.checked ? `Empty Tiles Removed: ${emptyTiles}` : null,
elRemoveDuplicates.checked ? `Duplicate Tiles Removed: ${duplicateTiles}` : null,
hoveredTileX >= 0 && hoveredTileY >= 0 ? `Hovered tile: (${hoveredTileX}, ${hoveredTileY}) (Index ${hoveredTileX + hoveredTileY * cols})` : null,
tileStatus[hoveredTileY * cols + hoveredTileX] || null
].filter(Boolean).join('\n');
elOutputPreview.querySelectorAll('canvas').forEach(canvas => {
const x = parseInt(canvas.getAttribute('data-tile-x'), 10);
const y = parseInt(canvas.getAttribute('data-tile-y'), 10);
const isHovered = x === hoveredTileX && y === hoveredTileY;
canvas.style.border = `1px solid ${isHovered ? 'red' : 'black'}`;
});
}
const updateOutput = () => {
// Clear previous output
elOutputPreview.innerHTML = '';
tileCanvases = [];
btnDownloadTiles.disabled = true;
hoveredTileX = -1;
hoveredTileY = -1;
// Image ready?
if(!image) {
updateOutputInformation();
return;
}
// For each tile, create a canvas and draw the tile on it
tileWidth = parseInt(elTileWidth.value, 10);
tileHeight = parseInt(elTileHeight.value, 10);
tilePadding = parseInt(elTilePadding.value, 10);
tileMargin = parseInt(elTileMargin.value, 10);
cols = Math.floor((image.width - tileMargin * 2 + tilePadding) / (tileWidth + tilePadding));
rows = Math.floor((image.height - tileMargin * 2 + tilePadding) / (tileHeight + tilePadding));
if(isNaN(cols) || isNaN(rows) || cols <= 0 || rows <= 0) {
elError.textContent = 'Invalid tile settings for the given image';
elError.style.display = 'block';
return;
}
emptyTiles = 0;
duplicateTiles = 0;
oldIndexToNewIndex = {};
tileStatus = {};
for(let y = 0; y < rows; y++) {
for(let x = 0; x < cols; x++) {
const tileCanvas = document.createElement('canvas');
tileCanvas.width = tileWidth;
tileCanvas.height = tileHeight;
const ctx = tileCanvas.getContext('2d');
ctx.drawImage(
image,
tileMargin + x * (tileWidth + tilePadding),
tileMargin + y * (tileHeight + tilePadding),
tileWidth,
tileHeight,
0,
0,
tileWidth,
tileHeight
);
tileCanvas.setAttribute('data-tile-x', x);
tileCanvas.setAttribute('data-tile-y', y);
// Empty check.
if(elRemoveEmpty.checked) {
const imageData = ctx.getImageData(0, 0, tileWidth, tileHeight);
const isEmpty = !imageData.data.some(channel => channel !== 0);
if(isEmpty) {
tileStatus[y * cols + x] = 'Removed (Empty)';
emptyTiles++;
continue;
}
}
if(elRemoveDuplicates.checked) {
const tileDataURL = tileCanvas.toDataURL();
const duplicateIndex = tileCanvases.findIndex(canvas => canvas.toDataURL() === tileDataURL);
if(duplicateIndex !== -1) {
tileStatus[y * cols + x] = `Removed (Duplicate of ${duplicateIndex})`;
duplicateTiles++;
continue;
}
}
tileCanvases.push(tileCanvas);
elOutputPreview.appendChild(tileCanvas);
}
}
btnDownloadTiles.disabled = false;
updateOutputInformation();
}
const onImage = img => {
elError.style.display = 'none';
image = img;
// Update input preview
elInputPreview.width = img.width;
elInputPreview.height = img.height;
const ctx = elInputPreview.getContext('2d');
ctx.drawImage(img, 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) {
onBadFile('No file selected');
return;
}
if(!file.type.startsWith('image/')) {
onBadFile('Selected file is not an image');
return;
}
elError.style.display = 'none';
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => onImage(img);
img.onerror = () => onBadFile('Failed to load image');
img.src = event.target.result;
};
reader.onerror = () => onBadFile('Failed to read file');
reader.readAsDataURL(file);
updateOutput();
}
// Listeners
elFile.addEventListener('change', (event) => {
onFile(event?.target?.files[0]);
});
elTileWidth.addEventListener('input', updateOutput);
elTileHeight.addEventListener('input', updateOutput);
elTilePadding.addEventListener('input', updateOutput);
elTileMargin.addEventListener('input', updateOutput);
elRemoveEmpty.addEventListener('change', updateOutput);
elRemoveDuplicates.addEventListener('change', updateOutput);
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';
});
btnDownloadTiles.addEventListener('click', () => {
if(tileCanvases.length === 0) {
alert('No tiles to download');
return;
}
const zip = new JSZip();
tileCanvases.forEach((canvas, index) => {
const dataURL = canvas.toDataURL('image/png');
const base64Data = dataURL.split(',')[1];
zip.file(`tile_${index}.png`, base64Data, {base64: true});
});
zip.generateAsync({type: 'blob'}).then(content => {
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = 'tiles.zip';
link.click();
});
});
// On mouse move of input preview
elInputPreview.addEventListener('mousemove', (event) => {
if(!image || elError.style.display !== 'none') return;
const rect = elInputPreview.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
hoveredTileX = Math.floor((x - tileMargin) / (tileWidth + tilePadding));
hoveredTileY = Math.floor((y - tileMargin) / (tileHeight + tilePadding));
updateOutputInformation();
});
elInputPreview.addEventListener('mouseleave', () => {
hoveredTileX = -1;
hoveredTileY = -1;
updateOutputInformation();
});
updateOutput();
btnBackgroundCheckerboard.click();
</script>
<script type="text/javascript">
/*!
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
<http://stuartk.com/jszip>
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
JSZip uses the library pako released under the MIT license :
https://github.com/nodeca/pako/blob/main/LICENSE
*/
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=functLine truncated
name:e,dir:i,date:s,comment:t.comment||"",unixPermissions:t.unixPermissions,dosPermissions:t.dosPermissions}).pipe(o)}),o.entriesCount=h}catch(e){o.error(e)}return o}},{"../compressions":3,"./ZipFileWorker":8}],10:[function(e,t,r){"use strict";function n(){if(!(this instanceof n))return new n;if(arguments.length)throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");this.files=Object.create(null),this.comment=null,this.root="",this.clone=function(){var e=new n;for(var t in this)"function"!=typeof this[t]&&(e[t]=this[t]);return e}}(n.prototype=e("./object")).loadAsync=e("./load"),n.support=e("./support"),n.defaults=e("./defaults"),n.version="3.10.1",n.loadAsync=function(e,t){return(new n).loadAsync(e,t)},n.external=e("./external"),t.exports=n},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(e,t,r){"use strict";var u=e("./utils"),i=e("./external"),n=e("./utf8"),s=e("./zipEntries"),a=e("./stream/Crc32Probe"),l=e("./nodejsUtils");function f(n){return new i.Promise(function(e,t){var r=n.decompressed.getContentWorker().pipe(new a);r.on("error",function(e){t(e)}).on("end",function(){r.streamInfo.crc32!==n.decompressed.crc32?t(new Error("Corrupted zip : CRC32 mismatch")):e()}).resume()})}t.exports=function(e,o){var h=this;return o=u.extend(o||{},{base64:!1,checkCRC32:!1,optimizedBinaryString:!1,createFolders:!1,decodeFileName:n.utf8decode}),l.isNode&&l.isStream(e)?i.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")):u.prepareContent("the loaded zip file",e,!0,o.optimizedBinaryString,o.base64).then(function(e){var t=new s(o);return t.load(e),t}).then(function(e){var t=[i.Promise.resolve(e)],r=e.files;if(o.checkCRC32)for(var n=0;n<r.length;n++)t.push(f(r[n]));return i.Promise.all(t)}).then(function(e){for(var t=e.shift(),r=t.files,n=0;n<r.length;n++){var i=r[n],s=i.fileNameStr,a=u.resolve(i.fileNameStr);h.file(a,i.decompressed,{binary:!0,optimizedBinaryString:!0,date:i.date,dir:i.dir,comment:i.fileCommentStr.length?i.fileCommentStr:null,unixPermissions:i.unixPermissions,dosPermissions:i.dosPermissions,createFolders:o.createFolders}),i.dir||(h.file(a).unsafeOriginalName=s)}return t.zipComment.length&&(h.comment=t.zipComment),h})}},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../stream/GenericWorker");function s(e,t){i.call(this,"Nodejs stream input adapter for "+e),this._upstreamEnded=!1,this._bindStream(t)}n.inherits(s,i),s.prototype._bindStream=function(e){var t=this;(this._stream=e).pause(),e.on("data",function(e){t.push({data:e,meta:{percent:0}})}).on("error",function(e){t.isPaused?this.generatedError=e:t.error(e)}).on("end",function(){t.isPaused?t._upstreamEnded=!0:t.end()})},s.prototype.pause=function(){return!!i.prototype.pause.call(this)&&(this._stream.pause(),!0)},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(this._upstreamEnded?this.end():this._stream.resume(),!0)},t.exports=s},{"../stream/GenericWorker":28,"../utils":32}],13:[function(e,t,r){"use strict";var i=e("readable-stream").Readable;function n(e,t,r){i.call(this,t),this._helper=e;var n=this;e.on("data",function(e,t){n.push(e)||n._helper.pause(),r&&r(t)}).on("error",function(e){n.emit("error",e)}).on("end",function(){n.push(null)})}e("../utils").inherits(n,i),n.prototype._read=function(){this._helper.resume()},t.exports=n},{"../utils":32,"readable-stream":16}],14:[function(e,t,r){"use strict";t.exports={isNode:"undefined"!=typeof Buffer,newBufferFrom:function(e,t){if(Buffer.from&&Buffer.from!==Uint8Array.from)return Buffer.from(e,t);if("number"==typeof e)throw new Error('The "data" argument must not be a number');return new Buffer(e,t)},allocBuffer:function(e){if(Buffer.alloc)return Buffer.alloc(e);var t=new Buffer(e);return t.fill(0),t},isBuffer:function(e){return Buffer.isBuffer(e)},isStream:function(e){return e&&"function"==typeof e.on&&"function"==typeof e.pause&&"function"==typeof e.resume}}},{}],15:[function(e,t,r){"use strict";function s(e,t,r){var n,i=u.getTypeOf(t),s=u.extend(r||{},f);s.date=s.date||new Date,null!==s.compression&&(s.compression=s.compression.toUpperCase()),"string"==typeof s.unixPermissions&&(s.unixPermissions=parseInt(s.unixPermissions,8)),s.unixPermissions&&16384&s.unixPermissions&&(s.dir=!0),s.dosPermissions&&16&s.dosPermissions&&(s.dir=!0),s.dir&&(e=g(e)),s.createFolders&&(n=_(e))&&b.call(this,n,!0);var a="string"===i&&!1===s.binary&&!1===s.base64;r&&void 0!==r.binary||(s.binary=!a),(t instanceof c&&0===t.uncompressedSize||s.dir||!t||0===t.length)&&(s.base64=!1,s.binary=!0,t="",s.compression="STORE",i="string");var o=null;o=t instanceof c||t instanceof l?t:p.isNode&&p.isStream(t)?new m(e,t):u.prepareContent(e,t,s.binary,s.optimizedBinaryString,s.base64);var h=new d(e,o,s);this.files[e]=h}var i=e("./utf8"),u=e("./utils"),l=e("./stream/GenericWorker"),aLine truncated
,t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r<this._listeners[e].length;r++)this._listeners[e][r].call(this,t)},pipe:function(e){return e.Line truncated
Array(2*s);for(t=r=0;t<s;)if((n=e[t++])<128)a[r++]=n;else if(4<(i=u[n]))a[r++]=65533,t+=i-1;else{for(n&=2===i?31:3===i?15:7;1<i&&t<s;)n=n<<6|63&e[t++],i--;1<i?a[r++]=65533:n<65536?a[r++]=n:(n-=65536,a[r++]=55296|n>>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r<e.length;++r)t[r]=255&e.charCodeAt(r);return t}e("setimmediate"),a.newBlob=function(t,r){a.checkSupport("blob");try{return new Blob([t],{type:r})}catch(e){try{var n=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);return n.append(t),n.getBlob(r)}catch(e){throw new Error("Bug : can't construct the Blob.")}}};var i={stringifyByChunk:function(e,t,r){var n=[],i=0,s=e.length;if(s<=r)return String.fromCharCode.apply(null,e);for(;i<s;)"array"===t||"nodebuffer"===t?n.push(String.fromCharCode.apply(null,e.slice(i,Math.min(i+r,s)))):n.push(String.fromCharCode.apply(null,e.subarray(i,Math.min(i+r,s)))),i+=r;return n.join("")},stringifyByChar:function(e){for(var t="",r=0;r<e.length;r++)t+=String.fromCharCode(e[r]);return t},applyCanBeUsed:{uint8array:function(){try{return o.uint8array&&1===String.fromCharCode.apply(null,new Uint8Array(1)).length}catch(e){return!1}}(),nodebuffer:function(){try{return o.nodebuffer&&1===String.fromCharCode.apply(null,r.allocBuffer(1)).length}catch(e){return!1}}()}};function s(e){var t=65536,r=a.getTypeOf(e),n=!0;if("uint8array"===r?n=i.applyCanBeUsed.uint8array:"nodebuffer"===r&&(n=i.applyCanBeUsed.nodebuffer),n)for(;1<t;)try{return i.stringifyByChunk(e,r,t)}catch(e){t=Math.floor(t/2)}return i.stringifyByChar(e)}function f(e,t){for(var r=0;r<e.length;r++)t[r]=e[r];return t}a.applyFromCharCode=s;var c={};c.string={string:n,array:function(e){return l(e,new Array(e.length))},arraybuffer:function(e){return c.string.uint8array(e).buffer},uint8array:function(e){return l(e,new Uint8Array(e.length))},nodebuffer:function(e){return l(e,r.allocBuffer(e.length))}},c.array={string:s,array:n,arraybuffer:function(e){return new Uint8Array(e).buffer},uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(e)}},c.arraybuffer={string:function(e){return s(new Uint8Array(e))},array:function(e){return f(new Uint8Array(e),new Array(e.byteLength))},arraybuffer:n,uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(new Uint8Array(e))}},c.uint8array={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return e.buffer},uint8array:n,nodebuffer:function(e){return r.newBufferFrom(e)}},c.nodebuffer={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return c.nodebuffer.uint8array(e).buffer},uint8array:function(e){return f(e,new Uint8Array(e.length))},nodebuffer:n},a.transformTo=function(e,t){if(t=t||"",!e)return t;a.checkSupport(e);var r=a.getTypeOf(t);return c[r][e](t)},a.resolve=function(e){for(var t=e.split("/"),r=[],n=0;n<t.length;n++){var i=t[n];"."===i||""===i&&0!==n&&n!==t.length-1||(".."===i?r.pop():r.push(i))}return r.join("/")},a.getTypeOf=function(e){return"string"==typeof e?"string":"[object Array]"===Object.prototype.toString.call(e)?"array":o.nodebuffer&&r.isBuffer(e)?"nodebuffer":o.uint8array&&e instanceof Uint8Array?"uint8array":o.arraybuffer&&e instanceof ArrayBuffer?"arraybuffer":void 0},a.checkSupport=function(e){if(!o[e.toLowerCase()])throw new Error(e+" is not supported by this platform")},a.MAX_VALUE_16BITS=65535,a.MAX_VALUE_32BITS=-1,a.pretty=function(e){var t,r,n="";for(r=0;r<(e||"").length;r++)n+="\\x"+((t=e.charCodeAt(r))<16?"0":"")+t.toString(16).toUpperCase();return n},a.delay=function(e,t,r){setImmediate(function(){e.apply(r||null,t||[])})},a.inherits=functiLine truncated
relativeOffsetEndOfZip64CentralDir<0))throw new Error("Corrupted zip: can't find the ZIP64 end of central directory");this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.readBlockZip64EndOfCentral()}var r=this.centralDirOffset+this.centralDirSize;this.zip64&&(r+=20,r+=12+this.zip64EndOfCentralSize);var n=t-r;if(0<n)this.isSignature(t,s.CENTRAL_FILE_HEADER)||(this.reader.zero=n);else if(n<0)throw new Error("Corrupted zip: missing "+Math.abs(n)+" bytes.")},prepareReader:function(e){this.reader=n(e)},load:function(e){this.prepareReader(e),this.readEndOfCentral(),this.readCentralDir(),this.readLocalFiles()}},t.exports=h},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utils":32,"./zipEntry":34}],34:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),s=e("./utils"),i=e("./compressedObject"),a=e("./crc32"),o=e("./utf8"),h=e("./compressions"),u=e("./support");function l(e,t){this.options=e,this.loadOptions=t}l.prototype={isEncrypted:function(){return 1==(1&this.bitFlag)},useUTF8:function(){return 2048==(2048&this.bitFlag)},readLocalPart:function(e){var t,r;if(e.skip(22),this.fileNameLength=e.readInt(2),r=e.readInt(2),this.fileName=e.readData(this.fileNameLength),e.skip(r),-1===this.compressedSize||-1===this.uncompressedSize)throw new Error("Bug or corrupted zip : didn't get enough information from the central directory (compressedSize === -1 || uncompressedSize === -1)");if(null===(t=function(e){for(var t in h)if(Object.prototype.hasOwnProperty.call(h,t)&&h[t].magic===e)return h[t];return null}(this.compressionMethod)))throw new Error("Corrupted zip : compression "+s.pretty(this.compressionMethod)+" unknown (inner file : "+s.transformTo("string",this.fileName)+")");this.decompressed=new i(this.compressedSize,this.uncompressedSize,this.crc32,t,e.readData(this.compressedSize))},readCentralPart:function(e){this.versionMadeBy=e.readInt(2),e.skip(2),this.bitFlag=e.readInt(2),this.compressionMethod=e.readString(2),this.date=e.readDate(),this.crc32=e.readInt(4),this.compressedSize=e.readInt(4),this.uncompressedSize=e.readInt(4);var t=e.readInt(2);if(this.extraFieldsLength=e.readInt(2),this.fileCommentLength=e.readInt(2),this.diskNumberStart=e.readInt(2),this.internalFileAttributes=e.readInt(2),this.externalFileAttributes=e.readInt(4),this.localHeaderOffset=e.readInt(4),this.isEncrypted())throw new Error("Encrypted zip are not supported");e.skip(t),this.readExtraFields(e),this.parseZIP64ExtraField(e),this.fileComment=e.readData(this.fileCommentLength)},processAttributes:function(){this.unixPermissions=null,this.dosPermissions=null;var e=this.versionMadeBy>>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4<i;)t=e.readInt(2),r=e.readInt(2),n=e.readData(r),this.extraFields[t]={id:t,length:r,value:n};e.setIndex(i)},handleUTF8:function(){var e=u.uint8array?"uint8array":"array";if(this.useUTF8())this.fileNameStr=o.utf8decode(this.fileName),this.fileCommentStr=o.utf8decode(this.fileComment);else{var t=this.findExtraFieldUnicodePath();if(null!==t)this.fileNameStr=t;else{var r=s.transformTo(e,this.fileName);this.fileNameStr=this.loadOptions.decodeFileName(r)}var n=this.findExtraFieldUnicodeComment();if(null!==n)this.fileCommentStr=n;else{var i=s.transformTo(e,this.fileComment);this.fileCommentStr=this.loadOptions.decodeFileName(i)}}},findExtraFieldUnicodePath:function(){var e=this.extraFields[28789];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileName)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null},findExtraFieldUnicodeComment:function(){var e=this.extraFields[25461];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileComment)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null}},t.exports=l},{"./compressedObject":2,"./compressions":3,"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(e,t,r){"use strict";function n(e,t,r){this.name=e,this.dir=r.dir,this.date=r.date,this.comment=r.comment,this.unixPermissions=r.unixPermissions,this.dosPermissions=r.dosPermissions,this._data=t,this._dataBinary=r.binary,this.options={compression:r.compression,compressionOptions:r.compressionOptions}}var s=e("./stream/StreamHelper"),i=e("./stream/DataWoLine truncated
0,l.resolve(o,s))},function(e){i||(i=!0,l.reject(o,e))})}},o.race=function(e){var t=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var r=e.length,n=!1;if(!r)return this.resolve([]);var i=-1,s=new this(u);for(;++i<r;)a=e[i],t.resolve(a).then(function(e){n||(n=!0,l.resolve(s,e))},function(e){n||(n=!0,l.reject(s,e))});var a;return s}},{immediate:36}],38:[function(e,t,r){"use strict";var n={};(0,e("./lib/utils/common").assign)(n,e("./lib/deflate"),e("./lib/inflate"),e("./lib/zlib/constants")),t.exports=n},{"./lib/deflate":39,"./lib/inflate":40,"./lib/utils/common":41,"./lib/zlib/constants":44}],39:[function(e,t,r){"use strict";var a=e("./zlib/deflate"),o=e("./utils/common"),h=e("./utils/strings"),i=e("./zlib/messages"),s=e("./zlib/zstream"),u=Object.prototype.toString,l=0,f=-1,c=0,d=8;function p(e){if(!(this instanceof p))return new p(e);this.options=o.assign({level:f,method:d,chunkSize:16384,windowBits:15,memLevel:8,strategy:c,to:""},e||{});var t=this.options;t.raw&&0<t.windowBits?t.windowBits=-t.windowBits:t.gzip&&0<t.windowBits&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=0;var r=a.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(r!==l)throw new Error(i[r]);if(t.header&&a.deflateSetHeader(this.strm,t.header),t.dictionary){var n;if(n="string"==typeof t.dictionary?h.string2buf(t.dictionary):"[object ArrayBuffer]"===u.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,(r=a.deflateSetDictionary(this.strm,n))!==l)throw new Error(i[r]);this._dict_set=!0}}function n(e,t){var r=new p(t);if(r.push(e,!0),r.err)throw r.msg||i[r.err];return r.result}p.prototype.push=function(e,t){var r,n,i=this.strm,s=this.options.chunkSize;if(this.ended)return!1;n=t===~~t?t:!0===t?4:0,"string"==typeof e?i.input=h.string2buf(e):"[object ArrayBuffer]"===u.call(e)?i.input=new Uint8Array(e):i.input=e,i.next_in=0,i.avail_in=i.input.length;do{if(0===i.avail_out&&(i.output=new o.Buf8(s),i.next_out=0,i.avail_out=s),1!==(r=a.deflate(i,n))&&r!==l)return this.onEnd(r),!(this.ended=!0);0!==i.avail_out&&(0!==i.avail_in||4!==n&&2!==n)||("string"===this.options.to?this.onData(h.buf2binstring(o.shrinkBuf(i.output,i.next_out))):this.onData(o.shrinkBuf(i.output,i.next_out)))}while((0<i.avail_in||0===i.avail_out)&&1!==r);return 4===n?(r=a.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===l):2!==n||(this.onEnd(l),!(i.avail_out=0))},p.prototype.onData=function(e){this.chunks.push(e)},p.prototype.onEnd=function(e){e===l&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Deflate=p,r.deflate=n,r.deflateRaw=function(e,t){return(t=t||{}).raw=!0,n(e,t)},r.gzip=function(e,t){return(t=t||{}).gzip=!0,n(e,t)}},{"./utils/common":41,"./utils/strings":42,"./zlib/deflate":46,"./zlib/messages":51,"./zlib/zstream":53}],40:[function(e,t,r){"use strict";var c=e("./zlib/inflate"),d=e("./utils/common"),p=e("./utils/strings"),m=e("./zlib/constants"),n=e("./zlib/messages"),i=e("./zlib/zstream"),s=e("./zlib/gzheader"),_=Object.prototype.toString;function a(e){if(!(this instanceof a))return new a(e);this.options=d.assign({chunkSize:16384,windowBits:0,to:""},e||{});var t=this.options;t.raw&&0<=t.windowBits&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(0<=t.windowBits&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),15<t.windowBits&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new i,this.strm.avail_out=0;var r=c.inflateInit2(this.strm,t.windowBits);if(r!==m.Z_OK)throw new Error(n[r]);this.header=new s,c.inflateGetHeader(this.strm,this.header)}function o(e,t){var r=new a(t);if(r.push(e,!0),r.err)throw r.msg||n[r.err];return r.result}a.prototype.push=function(e,t){var r,n,i,s,a,o,h=this.strm,u=this.options.chunkSize,l=this.options.dictionary,f=!1;if(this.ended)return!1;n=t===~~t?t:!0===t?m.Z_FINISH:m.Z_NO_FLUSH,"string"==typeof e?h.input=p.binstring2buf(e):"[object ArrayBuffer]"===_.call(e)?h.input=new Uint8Array(e):h.input=e,h.next_in=0,h.avail_in=h.input.length;do{if(0===h.avail_out&&(h.output=new d.Buf8(u),h.next_out=0,h.avail_out=u),(r=c.inflate(h,m.Z_NO_FLUSH))===m.Z_NEED_DICT&&l&&(o="string"==typeof l?p.string2buf(l):"[object ArrayBuffer]"===_.call(l)?new Uint8Array(l):l,r=c.inflateSetDictionary(this.strm,o)),r===m.Z_BUF_ERROR&&!0===f&&(r=m.Z_OK,f=!1),r!==m.Z_STREAM_END&&r!==m.Z_OK)return this.onEnd(r),!(this.ended=!0);h.next_out&&(0!==h.avail_out&&r!==m.Z_STREAM_END&&(0!==h.avail_in||n!==m.Z_FINISH&&n!==m.Z_SYNC_FLUSH)||("string"===this.options.to?(i=p.utf8border(h.output,h.next_out),s=h.next_out-i,a=p.buf2string(h.output,i),h.next_out=s,h.avail_out=u-s,s&&d.arraySet(h.output,h.output,i,s,0),this.onData(a)):this.onData(d.shrinkBuf(h.output,h.next_out)))),0===h.aLine truncated
e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&s<c);if(n=S-(c-s),s=c-S,a<n){if(e.match_start=t,o<=(a=n))break;d=u[s+a-1],p=u[s+a]}}}while((t=f[t&l])>h&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u<l&&(l=u),r=0===l?0:(a.avail_in-=l,c.arraySet(o,a.input,a.next_in,l,h),1===a.state.wrap?a.adler=d(a.adler,o,l,h):2===a.state.wrap&&(a.adler=p(a.adler,o,l,h)),a.next_in+=l,a.total_in+=l,l),e.lookahead+=r,e.lookahead+e.insert>=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+1])&e.hash_mask;e.insert&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+x-1])&e.hash_mask,e.prev[s&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=s,s++,e.insert--,!(e.lookahead+e.insert<x)););}while(e.lookahead<z&&0!==e.strm.avail_in)}function Z(e,t){for(var r,n;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!==r&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r)),e.match_length>=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart,0!=--e.match_length;);e.strstart++}else e.strstart+=e.match_length,e.match_length=0,e.ins_h=e.window[e.strstart],e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+1])&e.hash_mask;else n=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++;if(n&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=e.strstart<x-1?e.strstart:x-1,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}function W(e,t){for(var r,n,i;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),e.prev_length=e.match_length,e.prev_match=e.match_start,e.match_length=x-1,0!==r&&e.prev_length<e.max_lazy_match&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r),e.match_length<=5&&(1===e.strategy||e.match_length===x&&4096<e.strstart-e.match_start)&&(e.match_length=x-1)),e.prev_length>=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!=--e.prev_length;);if(e.match_available=0,e.match_length=x-1,e.strstart++,n&&(N(e,!1),0===e.strm.avail_out))return A}else if(e.match_available){if((n=u._tr_tally(e,0,e.window[e.strstart-1]))&&N(e,!1),e.strstart++,e.lookahead--,0===e.strm.avail_out)return A}else e.match_available=1,e.strstart++,e.lookahead--}return e.match_available&&(n=u._tr_tally(e,0,e.window[e.strstart-1]),e.match_available=0),e.insert=e.strstart<x-1?e.strstart:x-1,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}function M(e,t,r,n,i){this.good_length=e,this.max_lazy=t,this.nice_length=r,this.max_chain=n,this.func=i}function H(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=v,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,thisLine truncated
i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindex<n.gzhead.comment.length?255&n.gzhead.comment.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0<e.strstart&&(n=a[i=e.strstart-1])===a[++i]&&n===a[++i]&&n===a[++i]){s=e.strstart+S;do{}while(n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&i<s);e.match_length=S-(s-i),e.match_length>e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0<n.wrap&&(n.wrap=-n.wrap),0!==n.pending?m:1)},r.deflateEnd=function(e){var t;return e&&e.state?(t=e.state.status)!==C&&69!==t&&73!==t&&91!==t&&103!==t&&t!==E&&666!==t?R(e,_):(e.state=null,t===E?R(e,-3):m):_},r.deflateSetDictionary=function(e,t){var r,n,i,s,a,o,h,u,l=t.length;if(!e||!e.state)return _;if(2===(s=(r=e.state).wrap)||1===s&&r.status!==C||r.lookahead)return _;for(1===s&&(e.adler=d(e.adler,t,l,0)),r.wrap=0,l>=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<<r.hash_shift^r.window[n+x-1])&r.hash_mask,r.prev[n&r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=n,n++,--i;);r.strstart=n,r.lookahead=x-1,j(r)}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=x-1,r.match_available=0,e.next_in=o,e.input=h,e.avail_in=a,r.wrap=s,m},r.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./messages":51,"./trees":52}],47:[function(e,t,r){"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],48:[function(e,t,r){"use strict";t.exports=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C;r=e.state,n=e.next_in,z=e.input,i=n+(e.avail_in-5),s=e.next_out,C=e.output,a=s-(t-e.avail_out),o=s+(e.avail_out-257),h=r.dmax,u=r.wsize,l=r.whave,f=r.wnext,c=r.window,d=r.hold,p=r.bits,m=r.lencode,_=r.distcode,g=(1<<r.lenbits)-1,b=(1<<r.distbits)-1;e:do{p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=m[d&g];t:for(;;){if(d>>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<<y)-1)];continue t}if(32&y){r.mode=12;break e}e.msg="invalid literal/length code",r.mode=30;break e}w=65535&v,(y&=15)&&(p<y&&(d+=z[n++]<<p,p+=8),w+=d&(1<<y)-1,d>>>=y,p-=y),p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=_[d&b];r:for(;;){if(d>>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<<y)-1)];continue r}e.msg="invalid distance code",r.mode=30;break e}if(k=65535&v,p<(y&=15)&&(d+=z[n++]<<p,(p+=8)<y&&(d+=z[n++]<<p,p+=8)),h<(k+=d&(1<<y)-1)){e.msg="invalid distance too far back",r.mode=30;break e}if(d>>>=y,p-=y,(y=s-a)<k){if(l<(y=k-y)&&r.sane){e.msg="invalid distance too far back",r.mode=30;break e}if(S=c,(x=0)===f){if(x+=u-y,y<w){for(w-=y;C[s++]Line truncated
mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.name=null);r.length=0,r.mode=8;case 8:if(4096&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.comment+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.comment=null);r.mode=9;case 9:if(512&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(65535&r.check)){e.msg="header crc mismatch",r.mode=30;break}l=u=0}r.head&&(r.head.hcrc=r.flags>>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}e.adler=r.check=L(u),l=u=0,r.mode=11;case 11:if(0===r.havedict)return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,2;e.adler=r.check=1,r.mode=12;case 12:if(5===t||6===t)break e;case 13:if(r.last){u>>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}switch(r.last=1&u,l-=1,3&(u>>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if((65535&u)!=(u>>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o<d&&(d=o),h<d&&(d=h),0===d)break e;I.arraySet(i,n,s,d,a),o-=d,s+=d,h-=d,a+=d,r.length-=d;break}r.mode=12;break;case 17:for(;l<14;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.nlen=257+(31&u),u>>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286<r.nlen||30<r.ndist){e.msg="too many length or distance symbols",r.mode=30;break}r.have=0,r.mode=18;case 18:for(;r.have<r.ncode;){for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.lens[A[r.have++]]=7&u,u>>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have<r.nlen+r.ndist;){for(;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(b<16)u>>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u>>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=3+(7&(u>>>=_)),u>>>=3,l-=3}else{for(z=_+7;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=11+(127&(u>>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(g&&0==(240&g)){for(v=_,y=g,w=b;g=(C=r.lencode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.length+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<<r.distbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(0==(240&g)){for(v=_,y=g,w=b;g=(C=r.distcode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.offset+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)Line truncated
o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t<l;t++)e.dyn_ltree[2*t]=0;for(t=0;t<f;t++)e.dyn_dtree[2*t]=0;for(t=0;t<c;t++)e.bl_tree[2*t]=0;e.dyn_ltree[2*m]=1,e.opt_len=e.static_len=0,e.last_lit=e.matches=0}function M(e){8<e.bi_valid?U(e,e.bi_buf):0<e.bi_valid&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function H(e,t,r,n){var i=2*t,s=2*r;return e[i]<e[s]||e[i]===e[s]&&n[t]<=n[r]}function G(e,t,r){for(var n=e.heap[r],i=r<<1;i<=e.heap_len&&(i<e.heap_len&&H(t,e.heap[i+1],e.heap[i],e.depth)&&i++,!H(t,n,e.heap[i],e.depth));)e.heap[r]=e.heap[i],r=i,i<<=1;e.heap[r]=n}function K(e,t,r){var n,i,s,a,o=0;if(0!==e.last_lit)for(;n=e.pending_buf[e.d_buf+2*o]<<8|e.pending_buf[e.d_buf+2*o+1],i=e.pending_buf[e.l_buf+o],o++,0===n?L(e,i,t):(L(e,(s=A[i])+u+1,t),0!==(a=w[s])&&P(e,i-=I[s],a),L(e,s=N(--n),r),0!==(a=k[s])&&P(e,n-=T[s],a)),o<e.last_lit;);L(e,m,t)}function Y(e,t){var r,n,i,s=t.dyn_tree,a=t.stat_desc.static_tree,o=t.stat_desc.has_stree,h=t.stat_desc.elems,u=-1;for(e.heap_len=0,e.heap_max=_,r=0;r<h;r++)0!==s[2*r]?(e.heap[++e.heap_len]=u=r,e.depth[r]=0):s[2*r+1]=0;for(;e.heap_len<2;)s[2*(i=e.heap[++e.heap_len]=u<2?++u:0)]=1,e.depth[i]=0,e.opt_len--,o&&(e.static_len-=a[2*i+1]);for(t.max_code=u,r=e.heap_len>>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u<n||(e.bl_count[s]++,a=0,d<=n&&(a=c[n-d]),o=h[2*n],e.opt_len+=o*(s+a),f&&(e.static_len+=o*(l[2*n+1]+a)));if(0!==m){do{for(s=p-1;0===e.bl_count[s];)s--;e.bl_count[s]--,e.bl_count[s+1]+=2,e.bl_count[p]--,m-=2}while(0<m);for(s=p;0!==s;s--)for(n=e.bl_count[s];0!==n;)u<(i=e.heap[--r])||(h[2*i+1]!==s&&(e.opt_len+=(s-h[2*i+1])*h[2*i],h[2*i+1]=s),n--)}}(e,t),Z(s,u,e.bl_count)}function X(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),t[2*(r+1)+1]=65535,n=0;n<=r;n++)i=a,a=t[2*(n+1)+1],++o<h&&i===a||(o<u?e.bl_tree[2*i]+=o:0!==i?(i!==s&&e.bl_tree[2*i]++,e.bl_tree[2*b]++):o<=10?e.bl_tree[2*v]++:e.bl_tree[2*y]++,s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4))}function V(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),n=0;n<=r;n++)if(i=a,a=t[2*(n+1)+1],!(++o<h&&i===a)){if(o<u)for(;L(e,i,e.bl_tree),0!=--o;);else 0!==i?(i!==s&&(L(e,i,e.bl_tree),o--),L(e,b,e.bl_tree),P(e,o-3,2)):o<=10?(L(e,v,e.bl_tree),P(e,o-3,3)):(L(e,y,e.bl_tree),P(e,o-11,7));s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4)}}n(T);var q=!1;function J(e,t,r,n){P(e,(s<<1)+(n?1:0),3),function(e,t,r,n){M(e),n&&(U(e,r),U(e,~r)),i.arraySet(e.pending_buf,e.window,t,r,e.pending),e.pending+=r}(e,t,r,!0)}r._tr_init=function(e){q||(function(){var e,t,r,n,i,s=new Array(g+1);for(n=r=0;n<a-1;n++)for(I[n]=r,e=0;e<1<<w[n];e++)A[r++]=n;for(A[r-1]=n,n=i=0;n<16;n++)for(T[n]=i,e=0;e<1<<k[n];e++)E[i++]=n;for(i>>=7;n<f;n++)for(T[n]=i<<7,e=0;e<1<<k[n]-7;e++)E[256+i++]=n;for(t=0;t<=g;t++)s[t]=0;for(e=0;e<=143;)z[2*e+1]=8,e++,s[8]++;for(;e<=255;)z[2*e+1]=9,e++,s[9]++;for(;e<=279;)z[2*e+1]=7,e++,s[7]++;for(;e<=287;)z[2*e+1]=8,e++,s[8]++;for(Z(z,l+1,s),e=0;e<f;e++)C[2*e+1]=5,C[2*e]=j(e,5);O=new D(z,w,u+1,l,g),B=new D(C,k,0,f,g),R=new D(new Array(0),x,0,c,p)}(),q=!0),e.l_desc=new F(e.dyn_ltree,O),e.d_desc=new F(e.dyn_dtree,B),e.bl_desc=new F(e.bl_tree,R),e.bi_buf=0,e.bi_valid=0,W(e)},r._tr_stored_block=J,r._tr_flush_block=function(e,t,r,n){var i,s,a=0;0<e.level?(2===e.strm.data_type&&(e.strm.data_type=function(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t<u;t++)if(0!==e.dyn_ltree[2*t])return h;return o}(e)),Y(e,e.l_desc),Y(e,e.d_desc),a=function(e){var t;for(X(e,e.dyn_ltree,e.l_desc.max_code),X(e,e.dyn_dtree,e.d_desc.max_code),Y(e,e.bl_desc),t=c-1;3<=t&&0===e.bl_tree[2*S[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}(e),i=e.opt_len+3+7>>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i<n;i++)P(e,e.bl_tree[2*S[i]+1],3);V(e,e.dyn_ltree,t-1),V(e,e.dyn_dtree,r-1)}(e,e.l_desc.max_code+1,e.d_desc.max_code+1,a+1),K(e,e.dyn_ltree,e.dyn_dtree)),W(e),n&&M(e)},r._tr_tally=function(e,t,r){return e.pending_buf[e.d_buf+2*e.last_lit]=t>>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]+Line truncated
</script>
</html>
-546
View File
@@ -1,546 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dusk Tools / Tileset Creator</title>
<style type="text/css">
* {
box-sizing: border-box;
}
body {
font-size: 16px;
}
canvas {
image-rendering: pixelated;
}
</style>
</head>
<body>
<h1>Dusk Tileset Creator</h1>
<p>
Tool to create tilesets for textures. Currently only supports well sliced
tilesets (those with fixed dimensions essentially). In the future, may
support more freeform tilesets.
</p>
<div>
<h2>Tileset Settings</h2>
<div>
<button data-load-tileset>Load Tileset</button>
</div>
<div>
Define tile count by:
<div>
<label>
Tile Size
<input type="radio" name="define-by" value="size" checked />
</label>
<label>
Tile Count
<input type="radio" name="define-by" value="count" />
</label>
</div>
</div>
<div data-tile-sizes>
<div>
<label>Tile Width:</label>
<input type="number" value="8" data-tile-width min="1" step="1" />
</div>
<div>
<label>Tile Height:</label>
<input type="number" value="8" data-tile-height min="1" step="1" />
</div>
</div>
<div data-tile-counts style="display: none;">
<div>
<label>Column Count:</label>
<input type="number" value="10" data-column-count min="1" step="1" />
</div>
<div>
<label>Row Count:</label>
<input type="number" value="10" data-row-count min="1" step="1" />
</div>
</div>
<div>
<label>Unused Space on Right of Texture:</label>
<input type="number" value="0" data-right min="0" step="1" />
</div>
<div>
<label>Unused Space on Bottom of Texture:</label>
<input type="number" value="0" data-bottom min="0" step="1" />
</div>
</div>
<div>
<h2>Preview</h2>
<div>
<input type="file" data-texture-input />
</div>
<div data-output-error style="color:red;display:none;"></div>
<div>
<label>
Preview Scale:
<input type="number" value="4" data-indexed-preview-scale min="1" step="1" />
</label>
</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>
<canvas data-output-preview style="border:1px solid black;"></canvas>
</div>
<div>
<textarea data-output-information rows="15" style="width: 500px;"></textarea>
</div>
<div>
<button data-tileset-download>Download Tileset</button>
</div>
</div>
</body>
<script type="text/javascript">
// Element selectors
const elDefineBySize = document.querySelector('input[name="define-by"][value="size"]');
const elDefineByCount = document.querySelector('input[name="define-by"][value="count"]');
const elTileSizes = document.querySelector('[data-tile-sizes]');
const elTileCounts = document.querySelector('[data-tile-counts]');
const elTileWidth = document.querySelector('[data-tile-width]');
const elTileHeight = document.querySelector('[data-tile-height]');
const elColumnCount = document.querySelector('[data-column-count]');
const elRowCount = document.querySelector('[data-row-count]');
const elFileInput = document.querySelector('[data-texture-input]');
const elOutputError = document.querySelector('[data-output-error]');
const elOutputInformation = document.querySelector('[data-output-information]');
const elOutputPreview = document.querySelector('[data-output-preview]');
const elScale = document.querySelector('[data-indexed-preview-scale]');
const elRight = document.querySelector('[data-right]');
const elBottom = document.querySelector('[data-bottom]');
const btnDownloadTileset = document.querySelector('[data-tileset-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]');
const btnLoadTileset = document.querySelector('[data-load-tileset]');
// State
let imageWidth = 0;
let imageHeight = 0;
let pixels = null;
let hoveredX = -1;
let hoveredY = -1;
const getValues = () => {
if(!pixels) return null;
const right = parseInt(elRight.value) || 0;
const bottom = parseInt(elBottom.value) || 0;
let tileWidth, tileHeight, columnCount, rowCount;
if(elDefineBySize.checked) {
console.log('Defining by size');
tileWidth = parseInt(elTileWidth.value) || 0;
tileHeight = parseInt(elTileHeight.value) || 0;
columnCount = Math.floor((imageWidth - right) / tileWidth);
rowCount = Math.floor((imageHeight - bottom) / tileHeight);
} else {
console.log('Defining by count');
columnCount = parseInt(elColumnCount.value) || 0;
rowCount = parseInt(elRowCount.value) || 0;
tileWidth = Math.floor((imageWidth - right) / columnCount);
tileHeight = Math.floor((imageHeight - bottom) / rowCount);
}
const scale = parseInt(elScale.value) || 1;
const scaledWidth = imageWidth * scale;
const scaledHeight = imageHeight * scale;
const scaledTileWidth = tileWidth * scale;
const scaledTileHeight = tileHeight * scale;
const scaledRight = right * scale;
const scaledBottom = bottom * scale;
const u0 = (tileWidth / imageWidth);
const v0 = (tileHeight / imageHeight);
const hoveredTileX = isNaN(hoveredX) || hoveredX < 0 ? 0 : hoveredX;
const hoveredTileY = isNaN(hoveredY) || hoveredY < 0 ? 0 : hoveredY;
const hoveredU0 = hoveredTileX * u0;
const hoveredV0 = hoveredTileY * v0;
const hoveredU1 = hoveredU0 + u0;
const hoveredV1 = hoveredV0 + v0;
const hoveredTileIndex = hoveredTileY * columnCount + hoveredTileX;
return {
tileWidth,
tileHeight,
columnCount,
rowCount,
right,
bottom,
scale,
scaledWidth,
scaledHeight,
scaledTileWidth,
scaledTileHeight,
scaledRight,
scaledBottom,
u0,
v0,
hoveredU0,
hoveredV0,
hoveredU1,
hoveredV1,
hoveredTileX,
hoveredTileY,
hoveredTileIndex,
}
}
const updatePreview = () => {
const v = getValues();
if(!v) return;
// console.log('Updating preview with values', v);
// Prepare canvas
elOutputPreview.width = v.scaledWidth;
elOutputPreview.height = v.scaledHeight;
const ctx = elOutputPreview.getContext('2d');
ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height);
ctx.imageSmoothingEnabled = false;
// Resize pixels
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imageWidth;
tempCanvas.height = imageHeight;
const tempCtx = tempCanvas.getContext('2d');
const imageData = tempCtx.createImageData(imageWidth, imageHeight);
imageData.data.set(pixels);
tempCtx.putImageData(imageData, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, elOutputPreview.width, elOutputPreview.height);
// Draw blue overflow area for right and bottom cutoff
ctx.fillStyle = 'rgba(0,0,255,0.5)';
if(v.right > 0) {
ctx.fillRect(v.scaledWidth - v.scaledRight, 0, v.scaledRight, elOutputPreview.height);
}
if(v.bottom > 0) {
ctx.fillRect(0, v.scaledHeight - v.scaledBottom, elOutputPreview.width, v.scaledBottom);
}
// Draw red grid lines for tile boundaries
ctx.strokeStyle = 'rgba(255,0,0,1)';
for(let x = v.scaledTileWidth; x < elOutputPreview.width; x += v.scaledTileWidth) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, elOutputPreview.height);
ctx.stroke();
}
for(let y = v.scaledTileHeight; y < elOutputPreview.height; y += v.scaledTileHeight) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(elOutputPreview.width, y);
ctx.stroke();
}
elOutputInformation.value = [
v.hoveredX != -1 ? `Hovered Tile: ${v.hoveredTileX}, ${v.hoveredTileY} (${v.hoveredTileIndex})` : 'Hovered Tile: None',
v.hoveredX != -1 ? `Hovered UV: ${(v.hoveredU0).toFixed(4)}, ${(v.hoveredV0).toFixed(4)} -> ${(v.hoveredU1).toFixed(4)}, ${(v.hoveredV1).toFixed(4)}` : 'Hovered UV: None',
`Image Width: ${imageWidth}`,
`Image Height: ${imageHeight}`,
`Tile Width: ${v.tileWidth}`,
`Tile Height: ${v.tileHeight}`,
`Column Count: ${v.columnCount}`,
`uv: ${v.u0.toFixed(4)}, ${v.v0.toFixed(4)}`,
`Row Count: ${v.rowCount}`,
`Tile count: ${v.columnCount * v.rowCount}`,
].join('\n');
}
elTileWidth.addEventListener('input', updatePreview);
elTileHeight.addEventListener('input', updatePreview);
elColumnCount.addEventListener('input', updatePreview);
elRowCount.addEventListener('input', updatePreview);
elRight.addEventListener('input', updatePreview);
elBottom.addEventListener('input', updatePreview);
elScale.addEventListener('input', updatePreview);
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');
elDefineBySize.addEventListener('change', () => {
if(elDefineBySize.checked) {
elTileSizes.style.display = '';
elTileCounts.style.display = 'none';
}
updatePreview();
});
elDefineByCount.addEventListener('change', () => {
if(elDefineByCount.checked) {
elTileSizes.style.display = 'none';
elTileCounts.style.display = '';
}
updatePreview();
});
elOutputPreview.addEventListener('mousemove', (e) => {
const values = getValues();
if(!values) return;
const rect = elOutputPreview.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / values.scale);
const y = Math.floor((e.clientY - rect.top) / values.scale);
hoveredX = Math.floor(x / values.tileWidth);
hoveredY = Math.floor(y / values.tileHeight);
if(hoveredX < 0 || hoveredX >= values.columnCount || hoveredY < 0 || hoveredY >= values.rowCount) {
hoveredX = -1;
hoveredY = -1;
}
updatePreview();
});
elOutputPreview.addEventListener('mouseleave', () => {
hoveredX = -1;
hoveredY = -1;
updatePreview();
});
// File
elFileInput.addEventListener('change', (e) => {
elOutputError.style.display = 'none';
pixels = null;
if(!elFileInput.files.length) {
elOutputError.textContent = 'No file selected';
elOutputError.style.display = 'block';
return;
}
const file = elFileInput.files[0];
if(file.name.endsWith('.dpt')) {
// Load DPT file
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const data = new Uint8Array(arrayBuffer);
if(data[0] !== 'D'.charCodeAt(0) || data[1] !== 'P'.charCodeAt(0) || data[2] !== 'T'.charCodeAt(0)) {
elOutputError.textContent = 'Invalid DPT file';
elOutputError.style.display = 'block';
return;
} else if(data[3] !== 0x01) {
elOutputError.textContent = 'Unsupported DPT version';
elOutputError.style.display = 'block';
return;
}
// Begin color indexes
const width = (
data[4] |
(data[5] << 8) |
(data[6] << 16) |
(data[7] << 24)
)
const height = (
data[8] |
(data[9] << 8) |
(data[10] << 16) |
(data[11] << 24)
);
imageWidth = width;
imageHeight = height;
if(data.length < 12 + width * height) {
elOutputError.textContent = 'Invalid DPT file: not enough pixel data';
elOutputError.style.display = 'block';
return;
}
const uniqueIndexes = [];
for(let i = 0; i < width * height; i++) {
const colorIndex = data[12 + i];
if(!uniqueIndexes.includes(colorIndex)) {
uniqueIndexes.push(colorIndex);
}
}
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;
}
pixels = new Uint8Array(width * height * 4);
for(let i = 0; i < width * height; i++) {
const colorIndex = data[12 + i];
const color = adhocPalette[colorIndex];
pixels[i * 4] = color[0];
pixels[i * 4 + 1] = color[1];
pixels[i * 4 + 2] = color[2];
pixels[i * 4 + 3] = color[3];
}
updatePreview();
}
reader.onerror = () => {
elOutputError.textContent = 'Failed to read file';
elOutputError.style.display = 'block';
}
reader.readAsArrayBuffer(file);
} else {
const image = new Image();
image.onload = () => {
imageWidth = image.width;
imageHeight = image.height;
// Pixels
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imageWidth;
tempCanvas.height = imageHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(image, 0, 0);
const imageData = tempCtx.getImageData(0, 0, imageWidth, imageHeight);
pixels = imageData.data;
updatePreview();
};
image.onerror = () => {
pixels = null;
elOutputError.textContent = 'Failed to load image';
elOutputError.style.display = 'block';
updatePreview();
};
image.src = URL.createObjectURL(file);
}
});
btnDownloadTileset.addEventListener('click', () => {
const v = getValues();
if(!v) {
alert('No valid tileset to download');
return;
}
// Header: DTF0, tileWidth, tileHeight, columnCount, rowCount, right, bottom, u0, v0
const headerBytes = new Uint8Array([
'D'.charCodeAt(0), // Dusk
'T'.charCodeAt(0), // Tileset
'F'.charCodeAt(0), // File/Format
0x00, // version
v.tileWidth & 0xFF, (v.tileWidth >> 8) & 0xFF,
v.tileHeight & 0xFF, (v.tileHeight >> 8) & 0xFF,
v.columnCount & 0xFF, (v.columnCount >> 8) & 0xFF,
v.rowCount & 0xFF, (v.rowCount >> 8) & 0xFF,
v.right & 0xFF, (v.right >> 8) & 0xFF,
v.bottom & 0xFF, (v.bottom >> 8) & 0xFF,
...new Uint8Array(new Float32Array([v.u0]).buffer),
...new Uint8Array(new Float32Array([v.v0]).buffer),
]);
// Download file
const blob = new Blob([headerBytes], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tileset.dtf';
a.click();
URL.revokeObjectURL(url);
});
btnLoadTileset.addEventListener('click', () => {
// Browse for file.
const input = document.createElement('input');
input.type = 'file';
input.accept = '.dtf';
input.addEventListener('change', (e) => {
const files = e?.target?.files;
if(!files || !files.length || !files[0]) {
alert('No file selected');
return;
}
const file = files[0];
if(!file.name.endsWith('.dtf')) {
alert('Invalid file type. Please select a .dtf file.');
return;
}
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const data = new Uint8Array(arrayBuffer);
if(data[0] !== 'D'.charCodeAt(0) || data[1] !== 'T'.charCodeAt(0) || data[2] !== 'F'.charCodeAt(0)) {
alert('Invalid DTF file');
return;
}
if(data[3] !== 0x00) {
alert('Unsupported DTF version');
return;
}
const tileWidth = data[4] | (data[5] << 8);
const tileHeight = data[6] | (data[7] << 8);
const columnCount = data[8] | (data[9] << 8);
const rowCount = data[10] | (data[11] << 8);
const right = data[12] | (data[13] << 8);
const bottom = data[14] | (data[15] << 8);
// Switch to using size definition
elDefineBySize.checked = true;
elTileWidth.value = tileWidth;
elTileHeight.value = tileHeight;
elTileSizes.style.display = '';
elTileCounts.style.display = 'none';
elRight.value = right;
elBottom.value = bottom;
updatePreview();
};
reader.onerror = () => {
alert('Failed to read file');
};
reader.readAsArrayBuffer(file);
});
input.click();
});
// Init
btnBackgroundCheckerboard.click();
updatePreview();
</script>
</html>