Change to dynamic generation
7
.gitattributes
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.webp filter=lfs diff=lfs merge=lfs -text
|
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/dist
|
/dist
|
||||||
|
/scripts/node_modules
|
||||||
|
@ -13,7 +13,7 @@ Replaces the default bed textures with yiff. Supports 1.12+
|
|||||||
* [lime](https://e621.net/posts/1976008)
|
* [lime](https://e621.net/posts/1976008)
|
||||||
* [green](https://e621.net/posts/2416598)
|
* [green](https://e621.net/posts/2416598)
|
||||||
* [cyan](https://e621.net/posts/2364029)
|
* [cyan](https://e621.net/posts/2364029)
|
||||||
* [light_blue](https://e621.net/posts/2397890)
|
* [light_blue](https://e621.net/posts/2194206)
|
||||||
* [blue](https://e621.net/posts/314664)
|
* [blue](https://e621.net/posts/314664)
|
||||||
* [purple](https://e621.net/posts/1957635)
|
* [purple](https://e621.net/posts/1957635)
|
||||||
* [magenta](https://e621.net/posts/1933688)
|
* [magenta](https://e621.net/posts/1933688)
|
||||||
@ -38,7 +38,7 @@ Replaces the default bed textures with yiff. Supports 1.12+
|
|||||||
| Lime | ![Lime](examples/lime.png) | [Source](https://e621.net/posts/1976008) |
|
| Lime | ![Lime](examples/lime.png) | [Source](https://e621.net/posts/1976008) |
|
||||||
| Green | ![Green](examples/green.png) | [Source](https://e621.net/posts/2416598) |
|
| Green | ![Green](examples/green.png) | [Source](https://e621.net/posts/2416598) |
|
||||||
| Cyan | ![Cyan](examples/cyan.png) | [Source](https://e621.net/posts/2364029) |
|
| Cyan | ![Cyan](examples/cyan.png) | [Source](https://e621.net/posts/2364029) |
|
||||||
| Light Blue | ![Light Blue](examples/light_blue.png) | [Source](https://e621.net/posts/2397890) |
|
| Light Blue | ![Light Blue](examples/light_blue.png) | [Source](https://e621.net/posts/2194206) |
|
||||||
| Blue | ![Blue](examples/blue.png) | [Source](https://e621.net/posts/314664) |
|
| Blue | ![Blue](examples/blue.png) | [Source](https://e621.net/posts/314664) |
|
||||||
| Purple | ![Purple](examples/purple.png) | [Source](https://e621.net/posts/1957635) |
|
| Purple | ![Purple](examples/purple.png) | [Source](https://e621.net/posts/1957635) |
|
||||||
| Magenta | ![Magenta](examples/magenta.png) | [Source](https://e621.net/posts/1933688) |
|
| Magenta | ![Magenta](examples/magenta.png) | [Source](https://e621.net/posts/1933688) |
|
||||||
|
Before Width: | Height: | Size: 721 KiB |
Before Width: | Height: | Size: 738 KiB |
Before Width: | Height: | Size: 808 KiB |
Before Width: | Height: | Size: 676 KiB |
Before Width: | Height: | Size: 514 KiB |
Before Width: | Height: | Size: 885 KiB |
Before Width: | Height: | Size: 510 KiB |
Before Width: | Height: | Size: 803 KiB |
Before Width: | Height: | Size: 798 KiB |
Before Width: | Height: | Size: 865 KiB |
Before Width: | Height: | Size: 1012 KiB |
Before Width: | Height: | Size: 728 KiB |
Before Width: | Height: | Size: 784 KiB |
Before Width: | Height: | Size: 737 KiB |
Before Width: | Height: | Size: 651 KiB |
Before Width: | Height: | Size: 1.1 MiB |
BIN
common/pack.png
Before Width: | Height: | Size: 298 KiB |
BIN
data/assets/minecraft/textures/entity/bed/black.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/blue.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/brown.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/cyan.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/gray.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/green.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/light_blue.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/light_gray.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/lime.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/magenta.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/orange.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/pink.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/purple.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/red.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/white.png
(Stored with Git LFS)
Normal file
BIN
data/assets/minecraft/textures/entity/bed/yellow.png
(Stored with Git LFS)
Normal file
BIN
data/pack.png
(Stored with Git LFS)
Normal file
BIN
examples/all.png
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 431 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 429 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 538 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 439 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 131 B |
BIN
examples/red.png
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 524 KiB After Width: | Height: | Size: 131 B |
BIN
images/black.png
(Stored with Git LFS)
Normal file
BIN
images/blue.png
(Stored with Git LFS)
Normal file
BIN
images/brown.jpg
(Stored with Git LFS)
Normal file
BIN
images/cyan.png
(Stored with Git LFS)
Normal file
BIN
images/gray.png
(Stored with Git LFS)
Normal file
BIN
images/green.png
(Stored with Git LFS)
Normal file
91
images/images.json
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"defaultSize": [512, 512],
|
||||||
|
"details": {
|
||||||
|
"top": {
|
||||||
|
"pos": [10, 117],
|
||||||
|
"size": [204, 59],
|
||||||
|
"cut": [0, 0]
|
||||||
|
},
|
||||||
|
"middle": {
|
||||||
|
"pos": [10, 224],
|
||||||
|
"size": [204, 128],
|
||||||
|
"cut": [0, 59]
|
||||||
|
},
|
||||||
|
"bottom": {
|
||||||
|
"pos": [176, 186],
|
||||||
|
"size": [128, 38],
|
||||||
|
"cut": [38, 187],
|
||||||
|
"flip": "horizontal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"name": "black",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "blue",
|
||||||
|
"sizeMultiplier": 4,
|
||||||
|
"rotate": -90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brown",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cyan",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gray",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "green",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "light_blue",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "light_gray",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lime",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "magenta",
|
||||||
|
"sizeMultiplier": 4,
|
||||||
|
"rotate": -90,
|
||||||
|
"flip": "horizontal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orange",
|
||||||
|
"sizeMultiplier": 4,
|
||||||
|
"rotate": -90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pink",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "purple",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "red",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "white",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yellow",
|
||||||
|
"sizeMultiplier": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
images/light_blue.png
(Stored with Git LFS)
Normal file
BIN
images/light_gray.jpg
(Stored with Git LFS)
Normal file
BIN
images/lime.png
(Stored with Git LFS)
Normal file
BIN
images/magenta.png
(Stored with Git LFS)
Normal file
BIN
images/orange.jpg
(Stored with Git LFS)
Normal file
BIN
images/pink.png
(Stored with Git LFS)
Normal file
BIN
images/purple.png
(Stored with Git LFS)
Normal file
BIN
images/red.jpg
(Stored with Git LFS)
Normal file
29
images/test.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"defaultSize": [512, 512],
|
||||||
|
"details": {
|
||||||
|
"top": {
|
||||||
|
"pos": [10, 117],
|
||||||
|
"size": [204, 59],
|
||||||
|
"cut": [0, 0]
|
||||||
|
},
|
||||||
|
"middle": {
|
||||||
|
"pos": [10, 224],
|
||||||
|
"size": [204, 128],
|
||||||
|
"cut": [0, 59]
|
||||||
|
},
|
||||||
|
"bottom": {
|
||||||
|
"pos": [176, 186],
|
||||||
|
"size": [128, 38],
|
||||||
|
"cut": [38, 187],
|
||||||
|
"flip": "horizontal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"name": "animated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orange"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
images/white.png
(Stored with Git LFS)
Normal file
BIN
images/yellow.png
(Stored with Git LFS)
Normal file
@ -15,7 +15,8 @@
|
|||||||
["1.20", 15],
|
["1.20", 15],
|
||||||
["1.20.2", 18],
|
["1.20.2", 18],
|
||||||
["1.20.3", 26],
|
["1.20.3", 26],
|
||||||
["1.20.5", 32]
|
["1.20.5", 32],
|
||||||
|
["1.21", 34]
|
||||||
],
|
],
|
||||||
"exports": []
|
"exports": []
|
||||||
}
|
}
|
||||||
|
8
scripts/.eslintrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": ["@uwu-codes/eslint-config/esm"],
|
||||||
|
"rules": {
|
||||||
|
"unicorn/prevent-abbreviations": "off",
|
||||||
|
"unicorn/no-process-exit": "off",
|
||||||
|
"unicorn/import-style": "off"
|
||||||
|
}
|
||||||
|
}
|
BIN
scripts/bun.lockb
Executable file
292
scripts/common.ts
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { fileTypeFromBuffer } from "file-type";
|
||||||
|
import Ffmpeg from "fluent-ffmpeg";
|
||||||
|
import { rm } from "fs/promises";
|
||||||
|
import { readdir } from "fs/promises";
|
||||||
|
import { mkdtemp } from "fs/promises";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import { basename } from "path";
|
||||||
|
import sharp, { type OverlayOptions, type Sharp } from "sharp";
|
||||||
|
import type { Config, Detail, Image } from "./images";
|
||||||
|
|
||||||
|
async function makeComposite(image: string | Buffer, details: Detail, sizeMultiplier: number, offset = 0) {
|
||||||
|
let img = await sharp(image)
|
||||||
|
.extract({
|
||||||
|
top: details.cut[1] * sizeMultiplier,
|
||||||
|
left: details.cut[0] * sizeMultiplier,
|
||||||
|
width: details.size[0] * sizeMultiplier,
|
||||||
|
height: details.size[1] * sizeMultiplier
|
||||||
|
}).toBuffer();
|
||||||
|
if (details.rotate) {
|
||||||
|
img = await sharp(img).rotate(details.rotate).toBuffer();
|
||||||
|
}
|
||||||
|
if (details.flip) {
|
||||||
|
img = await sharp(img).flip(details.flip === "horizontal").flop(details.flip === "vertical").toBuffer();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: img,
|
||||||
|
top: (details.pos[1] * sizeMultiplier) + offset,
|
||||||
|
left: details.pos[0] * sizeMultiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyOptions(image: string | Buffer, options: Image, width: number, height: number) {
|
||||||
|
let img = await sharp(image).toBuffer()
|
||||||
|
if (options.rotate) {
|
||||||
|
img = await sharp(img).rotate(options.rotate).toBuffer();
|
||||||
|
}
|
||||||
|
if (options.flip) {
|
||||||
|
img = await sharp(img).flip(options.flip === "horizontal").flop(options.flip === "vertical").toBuffer();
|
||||||
|
}
|
||||||
|
img = await sharp(img).resize(width, height, { fit: "fill" }).toBuffer();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function formatImage(name: string, baseFile: string, config: Config, image: string | string[]) {
|
||||||
|
const options = config.images.find(img => img.name === name)!;
|
||||||
|
const isImage = !Array.isArray(image) && ["image/png", "image/jpeg"].includes((await fileTypeFromBuffer(Buffer.isBuffer(image) ? image : await Bun.file(image).arrayBuffer()))!.mime);
|
||||||
|
let result: Sharp;
|
||||||
|
const multplier = options.sizeMultiplier ?? 1;
|
||||||
|
const cropWidth = Math.max(config.details.top.size[0], config.details.middle.size[0], config.details.bottom.size[0]) * multplier,
|
||||||
|
cropHeight = (config.details.top.size[1] + config.details.middle.size[1] + config.details.bottom.size[1]) * multplier,
|
||||||
|
fullWidth = Math.floor(config.defaultSize[0] * multplier),
|
||||||
|
fullHeight = Math.floor(config.defaultSize[1] * multplier);
|
||||||
|
const base = sharp(baseFile);
|
||||||
|
if (multplier !== 1) {
|
||||||
|
base.resize(fullWidth, fullHeight, {
|
||||||
|
kernel: sharp.kernel.nearest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const baseBuf = await base.toBuffer();
|
||||||
|
if (isImage) {
|
||||||
|
const img = await applyOptions(image, options, cropWidth, cropHeight);
|
||||||
|
result = await sharp(baseBuf)
|
||||||
|
.composite([
|
||||||
|
await makeComposite(img, config.details.top, multplier),
|
||||||
|
await makeComposite(img, config.details.middle, multplier),
|
||||||
|
await makeComposite(img, config.details.bottom, multplier)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
let tmpDir: string | undefined;
|
||||||
|
|
||||||
|
let files: string[] = [];
|
||||||
|
tmpDir = await mkdtemp(`${tmpdir()}/split-frames-`);
|
||||||
|
if (!Array.isArray(image)) {
|
||||||
|
let select = "";
|
||||||
|
if (Array.isArray(options.frames)) {
|
||||||
|
select = options.frames.map(f => `eq(n\\,${f})`).join("+");
|
||||||
|
} else if (options.frames) {
|
||||||
|
select = `gte(n\\, ${options.frames.start})${options.frames.end ? `*lte(n\\,${options.frames.end}` : ""})`;
|
||||||
|
} else {
|
||||||
|
select = "n";
|
||||||
|
}
|
||||||
|
console.debug("Input file %s is not an image, assuming we need to extract frames.", name);
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
Ffmpeg(image)
|
||||||
|
.videoFilter(`scale=${cropWidth}:${cropHeight},select='${select}'`)
|
||||||
|
.addOption("-vsync vfr")
|
||||||
|
.output(`${tmpDir}/frame%04d.png`)
|
||||||
|
.on("end", async() => {
|
||||||
|
files = (await readdir(tmpDir!)).filter(f => /frame\d+\.png/.test(f)).sort().map(f => `${tmpDir}/${f}`);
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.error("No frames extracted for %s.", name);
|
||||||
|
if (tmpDir) {
|
||||||
|
await rm(tmpDir, { recursive: true });
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("Frames extracted to %s, %d total for %s", tmpDir, files.length, name);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.on("error", async(err) => {
|
||||||
|
if (tmpDir) {
|
||||||
|
await rm(tmpDir, { recursive: true });
|
||||||
|
}
|
||||||
|
console.error("Failed to extract frames for %s.", name);
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for (const img of image) {
|
||||||
|
await sharp(await applyOptions(img, options, cropWidth, cropHeight)).toFile(`${tmpDir}/${basename(img)}`);
|
||||||
|
}
|
||||||
|
files = image.map(f => `${tmpDir}/${basename(f)}`).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: OverlayOptions[] = [];
|
||||||
|
for (const img of files) {
|
||||||
|
const i = files.indexOf(img);
|
||||||
|
console.log("Processing %s (%d/%d) for %s", img, i + 1, files.length, name);
|
||||||
|
parts.push(
|
||||||
|
{ input: baseBuf, top: fullHeight * i, left: 0 },
|
||||||
|
await makeComposite(img, config.details.top, multplier, fullHeight * i),
|
||||||
|
await makeComposite(img, config.details.middle, multplier, fullHeight * i),
|
||||||
|
await makeComposite(img, config.details.bottom, multplier, fullHeight * i)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await sharp({
|
||||||
|
create: {
|
||||||
|
width: fullWidth,
|
||||||
|
height: fullHeight * files.length,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
},
|
||||||
|
limitInputPixels: false
|
||||||
|
})
|
||||||
|
.composite(parts)
|
||||||
|
if (tmpDir) {
|
||||||
|
await rm(tmpDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isImage, result: result! };
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameSizePercent = 0.1;
|
||||||
|
async function createFrame(colors: number[], percent: number, width: number, height: number) {
|
||||||
|
let frameW = width, frameH = height, i = 0;
|
||||||
|
const parts: OverlayOptions[] = [];
|
||||||
|
const frameVH = Math.floor(Math.min(width, height) * percent);
|
||||||
|
while (frameW > 0) {
|
||||||
|
if (i > colors.length - 1) {
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
const color = colors[i], r = (color >> 16) & 0xFF, g = (color >> 8) & 0xFF, b = color & 0xFF,
|
||||||
|
w = width * frameSizePercent, h = frameVH;
|
||||||
|
parts.push({
|
||||||
|
input: {
|
||||||
|
create: {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
channels: 4,
|
||||||
|
background: { r, g, b, alpha: 255 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
top: 0,
|
||||||
|
// left to right
|
||||||
|
left: i * w
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
create: {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
channels: 4,
|
||||||
|
background: { r, g, b, alpha: 255 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
top: frameH - h,
|
||||||
|
// right to left
|
||||||
|
left: width - (i + 1) * w
|
||||||
|
});
|
||||||
|
frameW -= w;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i = 0;
|
||||||
|
colors = colors.toReversed();
|
||||||
|
while (frameH > 0) {
|
||||||
|
if (i > colors.length - 1) {
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
const color = colors[i], r = (color >> 16) & 0xFF, g = (color >> 8) & 0xFF, b = color & 0xFF,
|
||||||
|
w = frameVH, h = height * frameSizePercent;
|
||||||
|
parts.push({
|
||||||
|
input: {
|
||||||
|
create: {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
channels: 4,
|
||||||
|
background: { r, g, b, alpha: 255 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
top: i * h,
|
||||||
|
// top to bottom
|
||||||
|
left: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
create: {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
channels: 4,
|
||||||
|
background: { r, g, b, alpha: 255 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
top: height - (i + 1) * h,
|
||||||
|
// bottom to top
|
||||||
|
left: width - w
|
||||||
|
});
|
||||||
|
frameH -= h;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return sharp({
|
||||||
|
create: {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
},
|
||||||
|
limitInputPixels: false
|
||||||
|
}).composite(parts).png().toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rgb = (hex: number) => ({ r: (hex >> 16) & 0xFF, g: (hex >> 8) & 0xFF, b: hex & 0xFF });
|
||||||
|
const kzBorderColor = rgb(0x6B3F7F)
|
||||||
|
const kzFillColor = rgb(0xD67FFF);
|
||||||
|
const plankSize = 4;
|
||||||
|
// the things I do to not have to make this a file
|
||||||
|
const plank = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAB3RJTUUH6AUfFCAKmCc95gAAATJJREFUKM9lUsFqwzAMlSXFXRYIYQRy6GX/tq/Zh/ZQGKUEOi9WpOyg1jWpL1Gen+X3/BS+vz6TCDxW2zQAoGZZ1et6JRF0UiSab1skUrMkklV9O4k4omZ+hv2TVfsuOC8SAQAh+paazbdtGu6/fLos9wrD4+a1aFht8+J0WZzAx4/Dq4ck0jZNEgEI820b+7sQQmRnF1TNXIyaubaxB5cKAKDK3rIdntTiyhtNQ2wrnM/XXGvd+Vlt25nkaYg19XzNfRdKGkV6yYFrxwAwDVHNPDhHXFWhcds0u05PiwCRaOyfD5BV+XzNq22urzjZZVLjew9VAlDdwz/z6kz0UfG3KsGVAStK+i74RDEhtojQiXetJ5QQk0gkotccAIBCWNQAlgPhosaYH1HI75+9v6F7+AdOcdbDy169/gAAAABJRU5ErkJggg==", "base64");
|
||||||
|
export async function makeKZ(squareSize: number, gridSize = 16) {
|
||||||
|
const kzBorderSize = squareSize * 0.0625;
|
||||||
|
const kzPlank = await sharp(plank).resize(squareSize, squareSize).toBuffer();
|
||||||
|
const plankStart = gridSize - plankSize;
|
||||||
|
const kzParts: OverlayOptions[] = [];
|
||||||
|
// i = top to bottom
|
||||||
|
// j = left to right
|
||||||
|
for (let i = 0; i < gridSize; i++) {
|
||||||
|
for (let j = 0; j < gridSize; j++) {
|
||||||
|
if (i < plankSize && j >= plankStart) {
|
||||||
|
kzParts.push({
|
||||||
|
input: kzPlank,
|
||||||
|
top: squareSize * i,
|
||||||
|
left: squareSize * j
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
kzParts.push({
|
||||||
|
input: {
|
||||||
|
create: {
|
||||||
|
width: kzBorderSize,
|
||||||
|
height: squareSize,
|
||||||
|
channels: 3,
|
||||||
|
background: kzBorderColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
top: squareSize * i,
|
||||||
|
left: squareSize * j
|
||||||
|
},{
|
||||||
|
input: {
|
||||||
|
create: {
|
||||||
|
width: squareSize,
|
||||||
|
height: kzBorderSize,
|
||||||
|
channels: 3,
|
||||||
|
background: kzBorderColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
top: squareSize * i,
|
||||||
|
left: squareSize * j
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sharp({
|
||||||
|
create: {
|
||||||
|
width: squareSize * 16,
|
||||||
|
height: squareSize * 16,
|
||||||
|
channels: 3,
|
||||||
|
background: kzFillColor
|
||||||
|
},
|
||||||
|
limitInputPixels: false
|
||||||
|
}).composite(kzParts).png().toBuffer();
|
||||||
|
}
|
117
scripts/images.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import type { PathLike } from "fs";
|
||||||
|
import { access } from "fs/promises";
|
||||||
|
import { dirname, resolve } from "path";
|
||||||
|
import sharp, { type OverlayOptions } from "sharp";
|
||||||
|
import { parseArgs } from "util";
|
||||||
|
import { formatImage, makeKZ } from "./common";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { writeFile } from "fs/promises";
|
||||||
|
import { rm } from "fs/promises";
|
||||||
|
import { mkdir } from "fs/promises";
|
||||||
|
import { stat } from "fs/promises";
|
||||||
|
import { readdir } from "fs/promises";
|
||||||
|
|
||||||
|
const { values: args } = parseArgs({
|
||||||
|
args: Bun.argv,
|
||||||
|
options: {
|
||||||
|
images: {
|
||||||
|
type: "string",
|
||||||
|
short: "i",
|
||||||
|
default: "images/images.json"
|
||||||
|
},
|
||||||
|
imagedir: {
|
||||||
|
type: "string",
|
||||||
|
short: "d",
|
||||||
|
default: "images"
|
||||||
|
},
|
||||||
|
outdir: {
|
||||||
|
type: "string",
|
||||||
|
short: "o",
|
||||||
|
default: "data"
|
||||||
|
},
|
||||||
|
basefile: {
|
||||||
|
type: "string",
|
||||||
|
short: "b",
|
||||||
|
default: "base.png"
|
||||||
|
},
|
||||||
|
throw: {
|
||||||
|
type: "boolean",
|
||||||
|
short: "t",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
strict: true,
|
||||||
|
allowPositionals: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const dirExists = async(path: PathLike) => access(path).then(() => true, () => false);
|
||||||
|
const imagesPath = resolve(args.images ?? "images.json");
|
||||||
|
const imageDir = resolve(args.imagedir ?? dirname(imagesPath));
|
||||||
|
const baseFile = resolve(args.basefile ?? "base.png");
|
||||||
|
const outDir = resolve(args.outdir ?? "data");
|
||||||
|
const throwOnMissing = !!args.throw;
|
||||||
|
if (!await dirExists(imageDir)) {
|
||||||
|
console.error("Image directory %s does not exist.", imageDir);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ap = (p: string) => resolve(p, "assets/minecraft/textures/entity/bed");
|
||||||
|
// const ap = (p: string) => resolve(p, "../img");
|
||||||
|
await rm(`${outDir}/assets`, { recursive: true, force: true });
|
||||||
|
await mkdir(ap(outDir), { recursive: true });
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
animation?: { frametime?: number; };
|
||||||
|
frames?: { start: number; end?: number; } | Array<number>;
|
||||||
|
name: string;
|
||||||
|
sizeMultiplier?: number;
|
||||||
|
rotate?: number;
|
||||||
|
flip?: "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Detail {
|
||||||
|
pos: [number, number];
|
||||||
|
size: [number, number];
|
||||||
|
cut: [number, number];
|
||||||
|
rotate?: number;
|
||||||
|
flip?: "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Details {
|
||||||
|
top: Detail;
|
||||||
|
middle: Detail;
|
||||||
|
bottom: Detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
defaultSize: [width: number, height: number];
|
||||||
|
details: Details;
|
||||||
|
images: Array<Image>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await Bun.file(imagesPath).json() as Config;
|
||||||
|
|
||||||
|
for (const image of config.images) {
|
||||||
|
let img: string | Array<string>;
|
||||||
|
if (await stat(`${imageDir}/${image.name}`).then(s => s.isDirectory(), () => false)) {
|
||||||
|
img = await readdir(`${imageDir}/${image.name}`).then(files => files.map(f => `${imageDir}/${image.name}/${f}`).sort());
|
||||||
|
} else {
|
||||||
|
const [file] = await Array.fromAsync(new Bun.Glob(`${image.name}.*`).scan({ onlyFiles: true, cwd: imageDir }));
|
||||||
|
if (!file) {
|
||||||
|
console.error("Image %s does not exist.", image.name);
|
||||||
|
if (throwOnMissing) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
img = `${imageDir}/${file}`
|
||||||
|
}
|
||||||
|
console.log("Processing %s", image.name);
|
||||||
|
|
||||||
|
const { isImage, result } = await formatImage(image.name, baseFile, config, img);
|
||||||
|
await result.toFile(`${ap(outDir)}/${image.name}.png`);
|
||||||
|
if (!isImage) {
|
||||||
|
await writeFile(`${ap(outDir)}/${image.name}.png.mcmeta`, JSON.stringify({ animation: image.animation ?? {} }));
|
||||||
|
}
|
||||||
|
}
|
26
scripts/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "image-maker",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.24",
|
||||||
|
"@types/node": "^20.12.13",
|
||||||
|
"@uwu-codes/eslint-config": "^1.1.28",
|
||||||
|
"eslint": "^9.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"file-type": "^19.0.0",
|
||||||
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
|
"sharp": "^0.33.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"compile": "bun build ./images.ts --compile --outfile bin/make-images"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"make-images": "./images.ts"
|
||||||
|
}
|
||||||
|
}
|
22
scripts/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|