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
|
||||
/scripts/node_modules
|
||||
|
@ -13,7 +13,7 @@ Replaces the default bed textures with yiff. Supports 1.12+
|
||||
* [lime](https://e621.net/posts/1976008)
|
||||
* [green](https://e621.net/posts/2416598)
|
||||
* [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)
|
||||
* [purple](https://e621.net/posts/1957635)
|
||||
* [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) |
|
||||
| Green | ![Green](examples/green.png) | [Source](https://e621.net/posts/2416598) |
|
||||
| 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) |
|
||||
| Purple | ![Purple](examples/purple.png) | [Source](https://e621.net/posts/1957635) |
|
||||
| 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
19
info.json
@ -3,19 +3,20 @@
|
||||
"description": "Replaces bed textures with furry porn.",
|
||||
"url": "https://git.furry.cool/MCFurryPacks/YiffyBeds",
|
||||
"versions": [
|
||||
["1.12", 3],
|
||||
["1.13", 4],
|
||||
["1.15", 5],
|
||||
["1.16", 6],
|
||||
["1.17", 7],
|
||||
["1.18", 8],
|
||||
["1.19", 9],
|
||||
["1.12", 3],
|
||||
["1.13", 4],
|
||||
["1.15", 5],
|
||||
["1.16", 6],
|
||||
["1.17", 7],
|
||||
["1.18", 8],
|
||||
["1.19", 9],
|
||||
["1.19.3", 12],
|
||||
["1.19.4", 13],
|
||||
["1.20", 15],
|
||||
["1.20", 15],
|
||||
["1.20.2", 18],
|
||||
["1.20.3", 26],
|
||||
["1.20.5", 32]
|
||||
["1.20.5", 32],
|
||||
["1.21", 34]
|
||||
],
|
||||
"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
|
||||
}
|
||||
}
|