ddnet
Version:
A typescript npm package for interacting with data from ddnet.org
273 lines • 7.8 kB
JavaScript
import sharp from 'sharp';
import { DDNetError } from '../../util.js';
import { TeeSkin6 } from './TeeSkin6.js';
import { TeeSkin7, assertSkinPart } from './TeeSkin7.js';
/**
* Tee skin eye variants
*/
export var TeeSkinEyeVariant;
(function (TeeSkinEyeVariant) {
TeeSkinEyeVariant["normal"] = "eye-normal";
TeeSkinEyeVariant["angry"] = "eye-angry";
TeeSkinEyeVariant["pain"] = "eye-pain";
TeeSkinEyeVariant["happy"] = "eye-happy";
/**
* Unused, some 0.6 skins have them though
*/
TeeSkinEyeVariant["dead"] = "eye-dead";
TeeSkinEyeVariant["surprise"] = "eye-surprise";
TeeSkinEyeVariant["blink"] = "eye-blink";
})(TeeSkinEyeVariant || (TeeSkinEyeVariant = {}));
/**
* Gets width and height from an image buffer.
*
* @internal
*/
export async function getImgSize(
/**
* The image buffer to get dimensions from.
*/
buf) {
const meta = await sharp(buf).metadata();
const { width, height } = meta;
if (!width || !height)
throw new DDNetError('Image dimensions could not be found.', { buffer: buf, sharpMetadata: meta });
return { width, height };
}
/**
* For the tee body
* Reorder that the average grey is 192
*
* @see
* https://github.com/ddnet/ddnet/blob/master/src/game/client/components/skins.cpp#L224-L260
*
* @internal
*/
export async function reorderBody(
/**
* The buffer to work on.
*/
buf) {
const raw = await sharp(buf).raw().toBuffer({ resolveWithObject: true });
buf = raw.data;
const frequencies = new Array(256).fill(0);
const newWeight = 192;
const invNewWeight = 255 - newWeight;
let orgWeight = 0;
// Find most common frequency
for (let byte = 0; byte < buf.length; byte += 4) {
if (buf[byte + 3] > 128) {
frequencies[buf[byte]]++;
}
}
for (let i = 1; i < 256; i++) {
if (frequencies[orgWeight] < frequencies[i]) {
orgWeight = i;
}
}
// Reorder
const invOrgWeight = 255 - orgWeight;
for (let byte = 0; byte < buf.length; byte += 4) {
let v = buf[byte];
if (v <= orgWeight && orgWeight == 0) {
v = 0;
}
else if (v <= orgWeight) {
v = Math.trunc((v / orgWeight) * newWeight);
}
else if (invOrgWeight == 0) {
v = newWeight;
}
else {
v = Math.trunc(((v - orgWeight) / invOrgWeight) * invNewWeight + newWeight);
}
buf[byte] = v;
buf[byte + 1] = v;
buf[byte + 2] = v;
}
return await sharp(buf, {
raw: {
channels: 4,
width: raw.info.width,
height: raw.info.height
}
})
.png()
.toBuffer({ resolveWithObject: true });
}
/**
* Converts a buffer to grayscale.
*/
export async function convertToGrayscale(
/**
* The buffer to work on.
*/
buf,
/**
* If the buffer should be reordered by the {@link reorderBody} function.
*/
reorder = false) {
const raw = await sharp(buf).raw().toBuffer({ resolveWithObject: true });
buf = raw.data;
for (let byte = 0; byte < buf.length; byte += 4) {
const avg = Math.trunc((buf[byte] + buf[byte + 1] + buf[byte + 2]) / 3);
buf[byte] = avg;
buf[byte + 1] = avg;
buf[byte + 2] = avg;
}
let grayscaled = await sharp(buf, {
raw: {
channels: 4,
width: raw.info.width,
height: raw.info.height
}
})
.png()
.toBuffer({ resolveWithObject: true });
if (reorder)
grayscaled = await reorderBody(grayscaled.data);
return grayscaled;
}
/**
* Tint a buffer with a given HSLA color.
*/
export async function tint(
/**
* The buffer to work on.
*/
buf,
/**
* The HSLA color to tint the buffer with.
*/
hsla,
/**
* Set to true for tinting 0.7 skins.
*/
use7UnclampVal) {
const raw = await sharp(buf).raw().toBuffer({ resolveWithObject: true });
buf = raw.data;
const darkest6 = 50;
const darkest7 = (61 / 255) * 100;
const darkest = use7UnclampVal ? darkest7 : darkest6;
const { r, g, b, a } = HSLAToRGBA({ ...hsla, l: darkest + (hsla.l * (100 - darkest)) / 100 });
for (let byte = 0; byte < buf.length; byte += 4) {
if (buf[byte + 3] === 0)
continue; // Skip fully transparent pixels
buf[byte] = (buf[byte] * r) / 255;
buf[byte + 1] = (buf[byte + 1] * g) / 255;
buf[byte + 2] = (buf[byte + 2] * b) / 255;
buf[byte + 3] = buf[byte + 3] * a;
}
return await sharp(buf, {
raw: {
channels: 4,
width: raw.info.width,
height: raw.info.height
}
})
.png()
.toBuffer({ resolveWithObject: true });
}
/**
* Converts a TW color code into HSLA values.
*
* @remarks Ranges for each component are:
* Hue: 0-360
* Sat: 0-100
* Lht: 0-100
* Alpha: 0-1
*/
export function HSLAfromTWcode(
/**
* The TW custom color code.
*/
twCode,
/**
* Set to true if the TW has an encoded alpha value. (For example, 0.7 tees have an alpha value encoded for the marking color)
*/
hasAlpha) {
let { h, s, l, a } = { h: 0, s: 0, l: 0, a: 255 };
if (hasAlpha) {
a = (twCode >> 24) & 0xff;
twCode = twCode & 0x00ffffff;
}
h = (twCode >> 16) & 0xff;
s = (twCode >> 8) & 0xff;
l = twCode & 0xff;
if (h === 255)
h = 0;
h *= 360 / 255;
s *= 100 / 255;
l *= 100 / 255;
a /= 255;
return { h, s, l, a };
}
/**
* Converts an HSLA color to an RGBA color.
*
* @remarks Ranges for each component are:
* Red: 0-255
* Green: 0-255
* Blue: 0-255
* Alpha: 0-1
*/
export function HSLAToRGBA(
/**
* The HSLA values ordered array.
*/
hsla) {
let { h, s, l, a } = hsla;
s /= 100;
l /= 100;
const k = (n) => (n + h / 30) % 12;
const a2 = s * Math.min(l, 1 - l);
const f = (n) => l - a2 * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
const r = Math.round(f(0) * 255);
const g = Math.round(f(8) * 255);
const b = Math.round(f(4) * 255);
return { r, g, b, a };
}
/**
* Helper function to quickly render any tee using the skin data from any client on any server reported by the master server. Custom colors are automatically handled.
*/
export async function renderTee(
/**
* The skin data to use.
*/
skinData,
/**
* Render options excluding custom colors.
*/
renderOpts) {
if (!skinData)
return await new TeeSkin6().render(renderOpts);
if (skinData.name) {
return await new TeeSkin6({ skinResource: skinData.name }).render({
...renderOpts,
customColors: {
bodyTWcode: skinData.color_body,
feetTWcode: skinData.color_feet
}
});
}
if (renderOpts?.eyeVariant === TeeSkinEyeVariant.dead) {
renderOpts.eyeVariant = TeeSkinEyeVariant.normal;
}
return await new TeeSkin7({
body: !assertSkinPart('body', skinData.body?.name) ? undefined : skinData.body.name,
decoration: !assertSkinPart('decoration', skinData.decoration?.name) ? undefined : skinData.decoration.name,
eyes: !assertSkinPart('eyes', skinData.eyes?.name) ? undefined : skinData.eyes.name,
feet: !assertSkinPart('feet', skinData.feet?.name) ? undefined : skinData.feet.name,
marking: !assertSkinPart('marking', skinData.marking?.name) ? undefined : skinData.marking.name
}).render({
...renderOpts,
customColors: {
bodyTWcode: skinData.body?.color,
markingTWcode: skinData.marking?.color,
decorationTWcode: skinData.decoration?.color,
feetTWcode: skinData.feet?.color,
eyesTWcode: skinData.eyes?.color
}
});
}
//# sourceMappingURL=TeeSkinUtils.js.map