444 lines
19 KiB
JavaScript
444 lines
19 KiB
JavaScript
"use strict";
|
||
|
||
// Texture Studio maths (Texture tab): selections on RGBA pixels (magic wand, colour range, lasso,
|
||
// rectangle), recolouring a selection, resizing, and .dds writing the way the game's textures are
|
||
// stored (DXT1 / DXT3 / DXT5 or A8R8G8B8, with mip maps). Pure functions on Uint8(Clamped)Arrays,
|
||
// no DOM: the Texture tab draws, this computes.
|
||
|
||
const TexKit = (() => {
|
||
// =====================================================================
|
||
// selections: a mask of w × h bytes (0 = out, 255 = in, between = feathered)
|
||
// =====================================================================
|
||
const dist = (px, i, c) => Math.max(Math.abs(px[i] - c[0]), Math.abs(px[i + 1] - c[1]), Math.abs(px[i + 2] - c[2]), Math.abs(px[i + 3] - c[3]));
|
||
|
||
/** Pixels like the one at (x, y), within tol (0–255 per channel): touching it (contiguous) or anywhere. */
|
||
function wand(px, w, h, x, y, tol, contiguous = true) {
|
||
const out = new Uint8Array(w * h);
|
||
if (x < 0 || y < 0 || x >= w || y >= h) return out;
|
||
const s = (y * w + x) * 4;
|
||
const c = [px[s], px[s + 1], px[s + 2], px[s + 3]];
|
||
if (!contiguous) {
|
||
for (let i = 0; i < w * h; i++) if (dist(px, i * 4, c) <= tol) out[i] = 255;
|
||
return out;
|
||
}
|
||
const stack = new Int32Array(w * h);
|
||
let n = 0;
|
||
stack[n++] = y * w + x;
|
||
out[y * w + x] = 255;
|
||
while (n) {
|
||
const i = stack[--n];
|
||
const ix = i % w, iy = (i - ix) / w;
|
||
for (const j of [ix > 0 ? i - 1 : -1, ix < w - 1 ? i + 1 : -1, iy > 0 ? i - w : -1, iy < h - 1 ? i + w : -1]) {
|
||
if (j < 0 || out[j] || dist(px, j * 4, c) > tol) continue;
|
||
out[j] = 255;
|
||
stack[n++] = j;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** A polygon [[x, y]…] (image pixels) filled even-odd, sampled at pixel centres. */
|
||
function polygon(w, h, pts) {
|
||
const out = new Uint8Array(w * h);
|
||
if (pts.length < 3) return out;
|
||
const xs = [];
|
||
for (let y = 0; y < h; y++) {
|
||
const cy = y + 0.5;
|
||
xs.length = 0;
|
||
for (let i = 0; i < pts.length; i++) {
|
||
const [x0, y0] = pts[i], [x1, y1] = pts[(i + 1) % pts.length];
|
||
if ((y0 <= cy && y1 > cy) || (y1 <= cy && y0 > cy)) xs.push(x0 + ((cy - y0) / (y1 - y0)) * (x1 - x0));
|
||
}
|
||
xs.sort((a, b) => a - b);
|
||
for (let k = 0; k + 1 < xs.length; k += 2) {
|
||
const a = Math.max(0, Math.ceil(xs[k] - 0.5)), b = Math.min(w - 1, Math.floor(xs[k + 1] - 0.5));
|
||
for (let x = a; x <= b; x++) out[y * w + x] = 255;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const rect = (w, h, x0, y0, x1, y1) => polygon(w, h, [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]);
|
||
|
||
/** Mask a combined with b: "new" | "add" | "sub" | "and". */
|
||
function combine(a, b, mode) {
|
||
if (mode === "new" || !a) return b;
|
||
const out = new Uint8Array(a.length);
|
||
for (let i = 0; i < a.length; i++)
|
||
out[i] = mode === "add" ? Math.max(a[i], b[i]) : mode === "sub" ? Math.min(a[i], 255 - b[i]) : Math.min(a[i], b[i]);
|
||
return out;
|
||
}
|
||
|
||
const invert = (m) => m.map((v) => 255 - v);
|
||
|
||
/** Grows (r > 0) or shrinks (r < 0) a mask by |r| pixels (square neighbourhood). */
|
||
function grow(m, w, h, r) {
|
||
if (!r) return m;
|
||
const pick = r > 0 ? Math.max : Math.min;
|
||
let cur = m;
|
||
for (let it = 0; it < Math.abs(r); it++) {
|
||
const out = new Uint8Array(cur.length);
|
||
for (let y = 0; y < h; y++)
|
||
for (let x = 0; x < w; x++) {
|
||
let v = cur[y * w + x];
|
||
if (x > 0) v = pick(v, cur[y * w + x - 1]);
|
||
if (x < w - 1) v = pick(v, cur[y * w + x + 1]);
|
||
if (y > 0) v = pick(v, cur[(y - 1) * w + x]);
|
||
if (y < h - 1) v = pick(v, cur[(y + 1) * w + x]);
|
||
out[y * w + x] = v;
|
||
}
|
||
cur = out;
|
||
}
|
||
return cur;
|
||
}
|
||
|
||
/** Soft edges: a box blur of radius r (twice, about a Gaussian). */
|
||
function feather(m, w, h, r) {
|
||
if (!r) return m;
|
||
let a = Float32Array.from(m);
|
||
const pass = (src, horiz) => {
|
||
const out = new Float32Array(src.length);
|
||
const n = horiz ? w : h, lines = horiz ? h : w;
|
||
for (let l = 0; l < lines; l++) {
|
||
const at = (k) => (horiz ? l * w + k : k * w + l);
|
||
let sum = 0;
|
||
for (let k = -r; k <= r; k++) sum += src[at(Math.max(0, Math.min(n - 1, k)))];
|
||
for (let k = 0; k < n; k++) {
|
||
out[at(k)] = sum / (2 * r + 1);
|
||
sum += src[at(Math.min(n - 1, k + r + 1))] - src[at(Math.max(0, k - r))];
|
||
}
|
||
}
|
||
return out;
|
||
};
|
||
for (let i = 0; i < 2; i++) a = pass(pass(a, true), false);
|
||
return Uint8Array.from(a, (v) => Math.round(v));
|
||
}
|
||
|
||
const count = (m) => m.reduce((s, v) => s + (v >= 128 ? 1 : 0), 0);
|
||
|
||
// =====================================================================
|
||
// recolour
|
||
// =====================================================================
|
||
function rgb2hsl(r, g, b) {
|
||
r /= 255, g /= 255, b /= 255;
|
||
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 2;
|
||
if (mx === mn) return [0, 0, l];
|
||
const d = mx - mn, s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
|
||
const hh = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
||
return [hh / 6, s, l];
|
||
}
|
||
function hsl2rgb(hh, s, l) {
|
||
if (!s) return [l * 255, l * 255, l * 255];
|
||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
|
||
const f = (t) => {
|
||
t = ((t % 1) + 1) % 1;
|
||
return 255 * (t < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p);
|
||
};
|
||
return [f(hh + 1 / 3), f(hh), f(hh - 1 / 3)];
|
||
}
|
||
|
||
/** How bright the selection's brightest parts are (98th percentile of max(r, g, b)), for colourise. */
|
||
function peak(px, mask) {
|
||
const hist = new Uint32Array(256);
|
||
let n = 0;
|
||
for (let i = 0; i < px.length / 4; i++) {
|
||
if (mask && mask[i] < 128) continue;
|
||
hist[Math.max(px[i * 4], px[i * 4 + 1], px[i * 4 + 2])]++;
|
||
n++;
|
||
}
|
||
let acc = 0;
|
||
for (let v = 0; v < 256; v++) if ((acc += hist[v]) >= n * 0.98) return Math.max(1, v);
|
||
return 255;
|
||
}
|
||
|
||
/**
|
||
* src → dst (same size) with the selection recoloured; outside it dst = src. opts:
|
||
* { mode: "colorize" | "adjust", color: [r, g, b], strength (0–1), bright (−1…1), hue (−180…180°),
|
||
* sat (−1…1), light (−1…1), contrast (−1…1), ref (colorize: the source's peak) }
|
||
* colorize: each pixel's brightness (its max channel against the selection's brightest) times the
|
||
* colour, so a dark hair texture keeps its strands; adjust: hue / saturation / lightness shifts.
|
||
*/
|
||
function recolor(src, dst, mask, opts) {
|
||
const o = { mode: "colorize", color: [200, 160, 60], strength: 1, bright: 0, hue: 0, sat: 0, light: 0, contrast: 0, ...opts };
|
||
const ref = o.ref || peak(src, mask);
|
||
const gain = 1 + o.bright;
|
||
const k = 1 + o.contrast;
|
||
for (let i = 0; i < src.length / 4; i++) {
|
||
const p = i * 4, m = mask ? mask[i] / 255 : 1;
|
||
dst[p + 3] = src[p + 3];
|
||
if (!m) {
|
||
dst[p] = src[p], dst[p + 1] = src[p + 1], dst[p + 2] = src[p + 2];
|
||
continue;
|
||
}
|
||
let r = src[p], g = src[p + 1], b = src[p + 2];
|
||
if (o.mode === "colorize") {
|
||
const t = Math.max(r, g, b) / ref;
|
||
const c = o.color;
|
||
r = r + (c[0] * t * gain - r) * o.strength;
|
||
g = g + (c[1] * t * gain - g) * o.strength;
|
||
b = b + (c[2] * t * gain - b) * o.strength;
|
||
} else {
|
||
let [hh, s, l] = rgb2hsl(r, g, b);
|
||
hh += o.hue / 360;
|
||
s = o.sat >= 0 ? s + (1 - s) * o.sat : s * (1 + o.sat);
|
||
l = o.light >= 0 ? l + (1 - l) * o.light : l * (1 + o.light);
|
||
[r, g, b] = hsl2rgb(hh, Math.max(0, Math.min(1, s)), Math.max(0, Math.min(1, l)));
|
||
}
|
||
if (o.contrast) (r = (r - 128) * k + 128), (g = (g - 128) * k + 128), (b = (b - 128) * k + 128);
|
||
dst[p] = src[p] + (r - src[p]) * m;
|
||
dst[p + 1] = src[p + 1] + (g - src[p + 1]) * m;
|
||
dst[p + 2] = src[p + 2] + (b - src[p + 2]) * m;
|
||
}
|
||
return dst;
|
||
}
|
||
|
||
// =====================================================================
|
||
// sizes
|
||
// =====================================================================
|
||
const pow2 = (n) => 2 ** Math.floor(Math.log2(Math.max(1, n)));
|
||
|
||
/** RGBA w × h → nw × nh (area average going down, bilinear going up). */
|
||
function resize(px, w, h, nw, nh) {
|
||
if (nw === w && nh === h) return Uint8ClampedArray.from(px);
|
||
const out = new Uint8ClampedArray(nw * nh * 4);
|
||
const sx = w / nw, sy = h / nh;
|
||
for (let y = 0; y < nh; y++)
|
||
for (let x = 0; x < nw; x++) {
|
||
const acc = [0, 0, 0, 0];
|
||
let n = 0;
|
||
if (sx >= 1 && sy >= 1) {
|
||
for (let yy = Math.floor(y * sy); yy < Math.min(h, Math.ceil((y + 1) * sy)); yy++)
|
||
for (let xx = Math.floor(x * sx); xx < Math.min(w, Math.ceil((x + 1) * sx)); xx++) {
|
||
const p = (yy * w + xx) * 4;
|
||
for (let c = 0; c < 4; c++) acc[c] += px[p + c];
|
||
n++;
|
||
}
|
||
} else {
|
||
const fx = Math.max(0, (x + 0.5) * sx - 0.5), fy = Math.max(0, (y + 0.5) * sy - 0.5);
|
||
const x0 = Math.min(w - 1, Math.floor(fx)), y0 = Math.min(h - 1, Math.floor(fy));
|
||
const x1 = Math.min(w - 1, x0 + 1), y1 = Math.min(h - 1, y0 + 1), ax = fx - x0, ay = fy - y0;
|
||
for (const [xx, yy, f] of [[x0, y0, (1 - ax) * (1 - ay)], [x1, y0, ax * (1 - ay)], [x0, y1, (1 - ax) * ay], [x1, y1, ax * ay]]) {
|
||
const p = (yy * w + xx) * 4;
|
||
for (let c = 0; c < 4; c++) acc[c] += px[p + c] * f;
|
||
}
|
||
n = 1;
|
||
}
|
||
const q = (y * nw + x) * 4;
|
||
for (let c = 0; c < 4; c++) out[q + c] = acc[c] / n;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** Every pixel opaque / alpha only 0 or 255 / real alpha. */
|
||
function alphaKind(px) {
|
||
let binary = true, opaque = true;
|
||
for (let i = 3; i < px.length; i += 4) {
|
||
const a = px[i];
|
||
if (a !== 255) opaque = false;
|
||
if (a !== 0 && a !== 255) {
|
||
binary = false;
|
||
break;
|
||
}
|
||
}
|
||
return opaque ? "opaque" : binary ? "binary" : "alpha";
|
||
}
|
||
|
||
// =====================================================================
|
||
// DXT (BC1 / BC2 / BC3) encoding
|
||
// =====================================================================
|
||
const to565 = (r, g, b) => ((Math.round((r * 31) / 255) << 11) | (Math.round((g * 63) / 255) << 5) | Math.round((b * 31) / 255)) >>> 0;
|
||
const from565 = (c) => [(((c >> 11) & 31) * 255) / 31, (((c >> 5) & 63) * 255) / 63, ((c & 31) * 255) / 31];
|
||
|
||
/** The 16 pixels of block (bx, by), edge pixels repeated: Float32Array(64). */
|
||
function block(px, w, h, bx, by) {
|
||
const b = new Float32Array(64);
|
||
for (let j = 0; j < 4; j++)
|
||
for (let i = 0; i < 4; i++) {
|
||
const x = Math.min(w - 1, bx * 4 + i), y = Math.min(h - 1, by * 4 + j);
|
||
const p = (y * w + x) * 4, q = (j * 4 + i) * 4;
|
||
b[q] = px[p], b[q + 1] = px[p + 1], b[q + 2] = px[p + 2], b[q + 3] = px[p + 3];
|
||
}
|
||
return b;
|
||
}
|
||
|
||
/** Colour part of a block (8 bytes) into out at o; use: which of the 16 pixels count (DXT1 transparent ones don't). */
|
||
function colorBlock(b, out, o, dxt1Alpha) {
|
||
const use = [];
|
||
for (let k = 0; k < 16; k++) if (!dxt1Alpha || b[k * 4 + 3] >= 128) use.push(k);
|
||
const dv = new DataView(out.buffer, out.byteOffset);
|
||
if (!use.length) {
|
||
// all transparent: 3-colour mode, every index 3 (transparent black)
|
||
dv.setUint16(o, 0, true), dv.setUint16(o + 2, 0, true), dv.setUint32(o + 4, 0xffffffff, true);
|
||
return;
|
||
}
|
||
// principal axis of the colours (a few power iterations), endpoints at the extremes along it
|
||
const mean = [0, 0, 0];
|
||
for (const k of use) for (let c = 0; c < 3; c++) mean[c] += b[k * 4 + c] / use.length;
|
||
const cov = [0, 0, 0, 0, 0, 0];
|
||
for (const k of use) {
|
||
const r = b[k * 4] - mean[0], g = b[k * 4 + 1] - mean[1], bl = b[k * 4 + 2] - mean[2];
|
||
cov[0] += r * r, cov[1] += r * g, cov[2] += r * bl, cov[3] += g * g, cov[4] += g * bl, cov[5] += bl * bl;
|
||
}
|
||
let ax = [1, 1, 1];
|
||
for (let it = 0; it < 6; it++) {
|
||
const n = [cov[0] * ax[0] + cov[1] * ax[1] + cov[2] * ax[2], cov[1] * ax[0] + cov[3] * ax[1] + cov[4] * ax[2], cov[2] * ax[0] + cov[4] * ax[1] + cov[5] * ax[2]];
|
||
const l = Math.hypot(...n);
|
||
if (l < 1e-6) break;
|
||
ax = n.map((v) => v / l);
|
||
}
|
||
let lo = Infinity, hi = -Infinity, kl = use[0], kh = use[0];
|
||
for (const k of use) {
|
||
const t = (b[k * 4] - mean[0]) * ax[0] + (b[k * 4 + 1] - mean[1]) * ax[1] + (b[k * 4 + 2] - mean[2]) * ax[2];
|
||
if (t < lo) (lo = t), (kl = k);
|
||
if (t > hi) (hi = t), (kh = k);
|
||
}
|
||
// the endpoints a touch inside the extremes (less error for the middle colours)
|
||
const inset = (k, toward) => [0, 1, 2].map((c) => b[k * 4 + c] + (b[toward * 4 + c] - b[k * 4 + c]) / 16);
|
||
const e0 = inset(kh, kl), e1 = inset(kl, kh);
|
||
let c0 = to565(...e0), c1 = to565(...e1);
|
||
const threeColor = dxt1Alpha && use.length < 16;
|
||
if (threeColor ? c0 > c1 : c0 < c1) [c0, c1] = [c1, c0];
|
||
if (!threeColor && c0 === c1) {
|
||
// one colour: 4-colour mode needs c0 > c1
|
||
if (c1 > 0) c1--;
|
||
else c0++;
|
||
}
|
||
const p0 = from565(c0), p1 = from565(c1);
|
||
const pal = threeColor
|
||
? [p0, p1, p0.map((v, i) => (v + p1[i]) / 2)]
|
||
: [p0, p1, p0.map((v, i) => (2 * v + p1[i]) / 3), p0.map((v, i) => (v + 2 * p1[i]) / 3)];
|
||
let bits = 0;
|
||
for (let k = 15; k >= 0; k--) {
|
||
let idx;
|
||
if (threeColor && b[k * 4 + 3] < 128) idx = 3;
|
||
else {
|
||
let best = Infinity;
|
||
idx = 0;
|
||
for (let i = 0; i < pal.length; i++) {
|
||
const c = pal[i], dr = c[0] - b[k * 4], dg = c[1] - b[k * 4 + 1], db = c[2] - b[k * 4 + 2];
|
||
const d = dr * dr + dg * dg + db * db;
|
||
if (d < best) (best = d), (idx = i);
|
||
}
|
||
}
|
||
bits = (bits << 2) | idx;
|
||
}
|
||
dv.setUint16(o, c0, true);
|
||
dv.setUint16(o + 2, c1, true);
|
||
dv.setUint32(o + 4, bits >>> 0, true);
|
||
}
|
||
|
||
/** DXT5 alpha (8 bytes): two endpoints, 3-bit indices. */
|
||
function alphaBlock5(b, out, o) {
|
||
let lo = 255, hi = 0;
|
||
for (let k = 0; k < 16; k++) (lo = Math.min(lo, b[k * 4 + 3])), (hi = Math.max(hi, b[k * 4 + 3]));
|
||
lo = Math.round(lo), hi = Math.round(hi);
|
||
// a0 > a1: the 8-value mode (one alpha: a0 = a1, every pixel takes index 0 exactly)
|
||
const a0 = hi, a1 = lo;
|
||
const pal = [a0, a1];
|
||
for (let i = 1; i < 7; i++) pal.push(((7 - i) * a0 + i * a1) / 7);
|
||
out[o] = a0, out[o + 1] = a1;
|
||
// 16 indices × 3 bits: pixels 0–7 in the first 3 bytes, 8–15 in the next 3
|
||
for (let half = 0; half < 2; half++) {
|
||
let bits = 0;
|
||
for (let k = 7; k >= 0; k--) {
|
||
const a = b[(half * 8 + k) * 4 + 3];
|
||
let best = Infinity, idx = 0;
|
||
for (let i = 0; i < 8; i++) {
|
||
const d = Math.abs(pal[i] - a);
|
||
if (d < best) (best = d), (idx = i);
|
||
}
|
||
bits = (bits << 3) | idx;
|
||
}
|
||
for (let i = 0; i < 3; i++) out[o + 2 + half * 3 + i] = (bits >> (8 * i)) & 255;
|
||
}
|
||
}
|
||
|
||
/** DXT3 alpha (8 bytes): 4 bits a pixel. */
|
||
function alphaBlock3(b, out, o) {
|
||
for (let k = 0; k < 16; k += 2) out[o + k / 2] = (Math.round(b[k * 4 + 3] / 17) & 15) | ((Math.round(b[(k + 1) * 4 + 3] / 17) & 15) << 4);
|
||
}
|
||
|
||
/** One mip level → its DXT bytes. */
|
||
function dxt(px, w, h, kind) {
|
||
const bw = Math.max(1, Math.ceil(w / 4)), bh = Math.max(1, Math.ceil(h / 4));
|
||
const size = kind === "DXT1" ? 8 : 16;
|
||
const out = new Uint8Array(bw * bh * size);
|
||
for (let by = 0; by < bh; by++)
|
||
for (let bx = 0; bx < bw; bx++) {
|
||
const b = block(px, w, h, bx, by), o = (by * bw + bx) * size;
|
||
if (kind === "DXT1") colorBlock(b, out, o, true);
|
||
else {
|
||
(kind === "DXT5" ? alphaBlock5 : alphaBlock3)(b, out, o);
|
||
colorBlock(b, out, o + 8, false);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** "auto" → what the game would use: opaque or cut-out (transparent black) DXT1, real alpha DXT5. */
|
||
const pickFormat = (px, format) => (format && format !== "auto" ? format : alphaKind(px) === "alpha" ? "DXT5" : "DXT1");
|
||
|
||
/**
|
||
* RGBA → a .dds: { format: "auto" | "DXT1" | "DXT3" | "DXT5" | "ARGB", mips: true }. → { bytes, format, levels }
|
||
*/
|
||
function encodeDds(px, w, h, opts = {}) {
|
||
const format = pickFormat(px, opts.format);
|
||
const levels = [];
|
||
let cur = px, cw = w, ch = h;
|
||
for (;;) {
|
||
levels.push(format === "ARGB" ? argb(cur) : dxt(cur, cw, ch, format));
|
||
if (!opts.mips || (cw === 1 && ch === 1)) break;
|
||
const nw = Math.max(1, cw >> 1), nh = Math.max(1, ch >> 1);
|
||
cur = resize(cur, cw, ch, nw, nh);
|
||
(cw = nw), (ch = nh);
|
||
}
|
||
const total = levels.reduce((s, l) => s + l.length, 0);
|
||
const out = new Uint8Array(128 + total);
|
||
const dv = new DataView(out.buffer);
|
||
dv.setUint32(0, 0x20534444, true); // "DDS "
|
||
dv.setUint32(4, 124, true);
|
||
const compressed = format !== "ARGB";
|
||
// caps | height | width | pixel format, + pitch or linear size, + mip map count
|
||
dv.setUint32(8, 0x1007 | (compressed ? 0x80000 : 0x8) | (levels.length > 1 ? 0x20000 : 0), true);
|
||
dv.setUint32(12, h, true);
|
||
dv.setUint32(16, w, true);
|
||
dv.setUint32(20, compressed ? levels[0].length : w * 4, true);
|
||
dv.setUint32(28, levels.length > 1 ? levels.length : 0, true);
|
||
dv.setUint32(76, 32, true);
|
||
if (compressed) {
|
||
dv.setUint32(80, 0x4, true); // FOURCC
|
||
for (let i = 0; i < 4; i++) out[84 + i] = format.charCodeAt(i);
|
||
} else {
|
||
dv.setUint32(80, 0x41, true); // RGB | alpha pixels
|
||
dv.setUint32(88, 32, true);
|
||
dv.setUint32(92, 0x00ff0000, true);
|
||
dv.setUint32(96, 0x0000ff00, true);
|
||
dv.setUint32(100, 0x000000ff, true);
|
||
dv.setUint32(104, 0xff000000, true);
|
||
}
|
||
dv.setUint32(108, 0x1000 | (levels.length > 1 ? 0x400008 : 0), true); // texture (| mip map | complex)
|
||
let o = 128;
|
||
for (const l of levels) out.set(l, o), (o += l.length);
|
||
return { bytes: out, format, levels: levels.length };
|
||
}
|
||
|
||
function argb(px) {
|
||
const out = new Uint8Array(px.length);
|
||
for (let i = 0; i < px.length; i += 4) (out[i] = px[i + 2]), (out[i + 1] = px[i + 1]), (out[i + 2] = px[i]), (out[i + 3] = px[i + 3]);
|
||
return out;
|
||
}
|
||
|
||
/** What a .dds is stored as (its header): { format, w, h, mips }. */
|
||
function ddsInfo(bytes) {
|
||
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||
if (bytes.length < 128 || dv.getUint32(0, true) !== 0x20534444) return null;
|
||
const pf = dv.getUint32(80, true);
|
||
const format = pf & 4 ? String.fromCharCode(...bytes.subarray(84, 88)) : `RGB${dv.getUint32(88, true)}`;
|
||
return { format, w: dv.getUint32(16, true), h: dv.getUint32(12, true), mips: Math.max(1, dv.getUint32(28, true)) };
|
||
}
|
||
|
||
return { wand, polygon, rect, combine, invert, grow, feather, count, peak, recolor, rgb2hsl, hsl2rgb, resize, pow2, alphaKind, pickFormat, encodeDds, ddsInfo };
|
||
})();
|
||
|
||
if (typeof window !== "undefined") window.TexKit = TexKit;
|