ddnet
Version:
A typescript npm package for interacting with data from ddnet.org
220 lines • 6.6 kB
JavaScript
import sharp from 'sharp';
import { DDNetError } from '../../util.js';
import { Color } from './Color.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,
/**
* Wether to allow working with the alpha channel.
*/
allowAlpha = false) {
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 } = new Color({ ...hsla, l: darkest + (hsla.l * (100 - darkest)) / 100 }).to('rgba');
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;
if (allowAlpha)
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 });
}
/**
* 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