Add a few more mesh types

This commit is contained in:
2026-04-14 08:38:50 -05:00
parent 378227c377
commit 0b570b5fd6
23 changed files with 875 additions and 83 deletions
+10 -10
View File
@@ -35,9 +35,9 @@ const DTF = (() => {
// When format is FORMAT_ALPHA and redAsAlpha is true, the red channel is
// used as the alpha value instead of the actual alpha channel.
function encode(width, height, rgbaData, format, redAsAlpha) {
if (format === undefined) format = FORMAT_RGBA;
if(format === undefined) format = FORMAT_RGBA;
const bpp = BPP[format];
if (bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`);
if(bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`);
const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData);
const buf = new ArrayBuffer(HEADER_SIZE + width * height * bpp);
@@ -53,7 +53,7 @@ const DTF = (() => {
bytes[12] = format;
let dst = HEADER_SIZE;
for (let i = 0; i < width * height; i++) {
for(let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
@@ -82,13 +82,13 @@ const DTF = (() => {
const bytes = new Uint8Array(buffer);
const view = new DataView(buffer);
if (bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF");
if (bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) {
if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF");
if(bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) {
throw new Error("Invalid DTF magic bytes not a DTF file");
}
const version = bytes[3];
if (version !== VERSION) {
if(version !== VERSION) {
throw new Error(`Unsupported DTF version: 0x${version.toString(16).padStart(2, "0")}`);
}
@@ -97,18 +97,18 @@ const DTF = (() => {
const format = bytes[12];
const bpp = BPP[format];
if (bpp === undefined) {
if(bpp === undefined) {
throw new Error(`Unsupported DTF format: 0x${format.toString(16).padStart(2, "0")}`);
}
const expected = HEADER_SIZE + width * height * bpp;
if (bytes.length < expected) {
if(bytes.length < expected) {
throw new Error(`DTF pixel data truncated (expected ${expected} bytes, got ${bytes.length})`);
}
const rgba = new Uint8ClampedArray(width * height * 4);
let src = HEADER_SIZE;
for (let i = 0; i < width * height; i++) {
for(let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA:
@@ -141,7 +141,7 @@ const DTF = (() => {
const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData);
const out = new Uint8ClampedArray(width * height * 4);
for (let i = 0; i < width * height; i++) {
for(let i = 0; i < width * height; i++) {
const o = i * 4;
switch (format) {
case FORMAT_ALPHA: {
+3 -3
View File
@@ -11,14 +11,14 @@ const DuskPNG = (() => {
// Encode RGBA pixel data into a PNG Buffer via pngjs.
// Returns a Uint8Array (Buffer) if pngjs is available, otherwise null.
function encode(width, height, rgbaData) {
if (!_pngAvailable()) return null;
if(!_pngAvailable()) return null;
const png = new PNG({ width, height });
const src = rgbaData instanceof Uint8ClampedArray
? rgbaData
: new Uint8ClampedArray(rgbaData);
for (let i = 0; i < src.length; i++) {
for(let i = 0; i < src.length; i++) {
png.data[i] = src[i];
}
@@ -29,7 +29,7 @@ const DuskPNG = (() => {
function download(filename, width, height, rgbaData) {
const buf = encode(width, height, rgbaData);
if (buf) {
if(buf) {
// pngjs path
const blob = new Blob([buf], { type: "image/png" });
_triggerDownload(URL.createObjectURL(blob), filename);
+17 -17
View File
@@ -4,7 +4,7 @@
// Returns the smallest power of two >= n.
function nextPow2(n) {
if (n <= 0) return 1;
if(n <= 0) return 1;
let p = 1;
while (p < n) p <<= 1;
return p;
@@ -57,10 +57,10 @@ function computeOutputSize() {
let outW = state.width;
let outH = state.height;
// Apply minimum-size constraints first so pow2 rounding accounts for them.
if (state.minWidth4 && outW < 4) outW = 4;
if (state.minHeight4 && outH < 4) outH = 4;
if (state.padWidthPow2) outW = nextPow2(outW);
if (state.padHeightPow2) outH = nextPow2(outH);
if(state.minWidth4 && outW < 4) outW = 4;
if(state.minHeight4 && outH < 4) outH = 4;
if(state.padWidthPow2) outW = nextPow2(outW);
if(state.padHeightPow2) outH = nextPow2(outH);
return { outW, outH };
}
@@ -69,8 +69,8 @@ function buildPaddedPixels() {
const src = state.pixels;
// Zero-filled Uint8ClampedArray = fully transparent black padding.
const out = new Uint8ClampedArray(outW * outH * 4);
for (let y = 0; y < state.height; y++) {
for (let x = 0; x < state.width; x++) {
for(let y = 0; y < state.height; y++) {
for(let x = 0; x < state.width; x++) {
const si = (y * state.width + x) * 4;
const di = (y * outW + x) * 4;
out[di] = src[si];
@@ -87,8 +87,8 @@ function buildPaddedPixels() {
const CHECKER_CELL = 8;
function drawCheckerboard(w, h) {
for (let y = 0; y < h; y += CHECKER_CELL) {
for (let x = 0; x < w; x += CHECKER_CELL) {
for(let y = 0; y < h; y += CHECKER_CELL) {
for(let x = 0; x < w; x += CHECKER_CELL) {
ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff";
ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y));
}
@@ -96,7 +96,7 @@ function drawCheckerboard(w, h) {
}
function render() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height } = buildPaddedPixels();
const cw = width * state.scale;
@@ -105,7 +105,7 @@ function render() {
canvas.width = cw;
canvas.height = ch;
if (state.bg === "grid") {
if(state.bg === "grid") {
drawCheckerboard(cw, ch);
} else {
ctx.fillStyle = state.bg;
@@ -122,7 +122,7 @@ function render() {
// ─── Info update ─────────────────────────────────────────────────────────────
function updateInfo() {
if (!state.pixels) return;
if(!state.pixels) return;
const { outW, outH } = computeOutputSize();
const bytes = DTF.HEADER_SIZE + outW * outH * DTF.BPP[state.format];
infoInputSize.textContent = `${state.width} × ${state.height}`;
@@ -189,8 +189,8 @@ function applyImageData(width, height, data, filename) {
}
function handleFile(file) {
if (!file) return;
if (file.name.toLowerCase().endsWith(".dtf")) {
if(!file) return;
if(file.name.toLowerCase().endsWith(".dtf")) {
loadDTF(file);
} else {
loadStandardImage(file);
@@ -200,7 +200,7 @@ function handleFile(file) {
// ─── Export ───────────────────────────────────────────────────────────────────
function exportDTF() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height } = buildPaddedPixels();
const buf = DTF.encode(width, height, pixels, state.format);
const blob = new Blob([buf], { type: "application/octet-stream" });
@@ -214,7 +214,7 @@ function exportDTF() {
}
function exportPNG() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height } = buildPaddedPixels();
DuskPNG.download(`${state.filename}.png`, width, height, pixels);
}
@@ -232,7 +232,7 @@ scaleInput.addEventListener("input", () => {
bgSwatches.addEventListener("click", e => {
const btn = e.target.closest(".bg-swatch");
if (!btn) return;
if(!btn) return;
bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
state.bg = btn.dataset.bg;
+17 -17
View File
@@ -43,8 +43,8 @@ const redAsAlphaCheck = document.getElementById("red-as-alpha");
const CHECKER_CELL = 8;
function drawCheckerboard(w, h) {
for (let y = 0; y < h; y += CHECKER_CELL) {
for (let x = 0; x < w; x += CHECKER_CELL) {
for(let y = 0; y < h; y += CHECKER_CELL) {
for(let x = 0; x < w; x += CHECKER_CELL) {
ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff";
ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y));
}
@@ -52,7 +52,7 @@ function drawCheckerboard(w, h) {
}
function render() {
if (!state.pixels) return;
if(!state.pixels) return;
const { pixels, width, height, scale, bg, format } = state;
const cw = width * scale;
@@ -62,7 +62,7 @@ function render() {
canvas.height = ch;
// 1. Background
if (bg === "grid") {
if(bg === "grid") {
drawCheckerboard(cw, ch);
} else {
ctx.fillStyle = bg;
@@ -128,17 +128,17 @@ function loadDTF(file) {
function updateWarnings() {
const warnings = [];
if (state.pixels) {
if(state.pixels) {
const { width, height } = state;
const isPow2 = n => n > 0 && (n & (n - 1)) === 0;
if (width < 4) warnings.push(`Width is below 4 px (${width})`);
if (height < 4) warnings.push(`Height is below 4 px (${height})`);
if (!isPow2(width)) warnings.push(`Width is not a power of two (${width})`);
if (!isPow2(height)) warnings.push(`Height is not a power of two (${height})`);
if(width < 4) warnings.push(`Width is below 4 px (${width})`);
if(height < 4) warnings.push(`Height is below 4 px (${height})`);
if(!isPow2(width)) warnings.push(`Width is not a power of two (${width})`);
if(!isPow2(height)) warnings.push(`Height is not a power of two (${height})`);
const bytes = DTF.HEADER_SIZE + width * height * DTF.BPP[state.format];
if (bytes > 256 * 1024) {
if(bytes > 256 * 1024) {
warnings.push(`Output exceeds 256 KB (${(bytes / 1024).toFixed(1)} KB)`);
}
}
@@ -150,7 +150,7 @@ function updateWarnings() {
}
function updateDtfSize() {
if (!state.pixels) return;
if(!state.pixels) return;
const bytes = DTF.HEADER_SIZE + state.width * state.height * DTF.BPP[state.format];
infoDtfSize.textContent = `${(bytes / 1024).toFixed(1)} KB`;
updateWarnings();
@@ -163,7 +163,7 @@ function applyImageData(width, height, data, filename, formatLabel, format) {
state.filename = filename.replace(/\.[^/.]+$/, "");
// Sync format selector when loading an existing DTF
if (format !== undefined) {
if(format !== undefined) {
state.format = format;
formatSelect.value = format;
redAsAlphaRow.hidden = format !== DTF.FORMAT_ALPHA;
@@ -189,8 +189,8 @@ function applyImageData(width, height, data, filename, formatLabel, format) {
}
function handleFile(file) {
if (!file) return;
if (file.name.toLowerCase().endsWith(".dtf")) {
if(!file) return;
if(file.name.toLowerCase().endsWith(".dtf")) {
loadDTF(file);
} else {
loadStandardImage(file);
@@ -204,7 +204,7 @@ function showError(msg) {
// ─── Export ───────────────────────────────────────────────────────────────────
function exportDTF() {
if (!state.pixels) return;
if(!state.pixels) return;
const buf = DTF.encode(state.width, state.height, state.pixels, state.format, state.redAsAlpha);
const blob = new Blob([buf], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
@@ -217,7 +217,7 @@ function exportDTF() {
}
function exportPNG() {
if (!state.pixels) return;
if(!state.pixels) return;
DuskPNG.download(`${state.filename}.png`, state.width, state.height, state.pixels);
}
@@ -234,7 +234,7 @@ scaleInput.addEventListener("input", () => {
bgSwatches.addEventListener("click", e => {
const btn = e.target.closest(".bg-swatch");
if (!btn) return;
if(!btn) return;
bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
state.bg = btn.dataset.bg;