Skip to content

Instantly share code, notes, and snippets.

@3vorp
Last active July 16, 2024 18:49
Show Gist options
  • Save 3vorp/69eebe2f8a13770fa03841fc8f70b860 to your computer and use it in GitHub Desktop.
Save 3vorp/69eebe2f8a13770fa03841fc8f70b860 to your computer and use it in GitHub Desktop.
Configurable Minecraft nether portal atlas generation script for Node.js. Made possible by coyo_t's work and the Minecraft wiki (original code at http://catsofwar.com/mc_proctex/mc.html).
const { createCanvas, ImageData } = require("@napi-rs/canvas");
const { writeFileSync } = require("fs");
/**
* @typedef {Object} PortalOptions
* @property {Number} width
* @property {Number} height
* @property {Number} [frameCount]
* @property {Number} [spiralCount]
* @property {Number} [noise]
*/
const clamp = (x, mini, maxi) => Math.max(mini, Math.min(x, maxi));
function toRGBA(input) {
const r = Math.floor(input ** 2 * 200 + 55);
const g = Math.floor(input ** 4 * 255);
const b = Math.floor(input * 100 + 155);
// alpha channel is the same as blue
return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), clamp(b, 0, 255)];
}
/**
* Generates a nether portal frame at the given index and frametime
* @author Evorp, coyo_t, LethalChicken?
* @param {Number} index - Frame index
* @param {PortalOptions} options - Options for frame
* @returns {ImageData} Generated frame data
*/
function generatePortalFrame(index, options) {
const pixels = new ImageData(options.width, options.height);
// going pixel by pixel
for (let posX = 0; posX < options.width; ++posX) {
for (let posY = 0; posY < options.height; ++posY) {
// color value for the pixel as added sines
let output = 0;
// there's two main spirals so we iterate over both per-pixel
// there's the one in the middle and one offset to the top right
for (let spiralCount = 0; spiralCount < 2; ++spiralCount) {
// get offset for each spiral
let spiralX = ((posX - spiralCount * (options.width / 2)) / options.width) * 2;
let spiralY = ((posY - spiralCount * (options.height / 2)) / options.height) * 2;
// make sure the offset spiral appears on every corner
spiralX += ((spiralX < -1) - (spiralX >= 1)) * 2;
spiralY += ((spiralY < -1) - (spiralY >= 1)) * 2;
const distance = spiralX ** 2 + spiralY ** 2;
// https://en.wikipedia.org/wiki/Atan2#/media/File:Atan2definition.svg
let spiral = Math.atan2(spiralY, spiralX);
spiral +=
// percentage of frames done
((index / options.frameCount) *
// convert percentage into radians (2pi = 360˚)
Math.PI *
2 -
// creates the alternating stripes
distance * options.spiralCount +
// make the center spiral a bit bigger
spiralCount * 2) *
// mirror direction of corner spiral
(spiralCount * 2 - 1);
// converts the radians into ratio, and clamps to [0, 1]
spiral = Math.sin(spiral) * 0.5 + 0.5;
// decrease color intensity depending on distance from center
spiral /= distance + 1;
// tone down the colors a bit more before adding the value
output += spiral * 0.5;
}
// add a tiny bit of noise so the added sines aren't too smooth
output += Math.random() * options.noise;
// convert sines to rgba
const converted = toRGBA(output);
// multiply by length of [r, g, b, a] array
const pxi = (posY * options.width + posX) * 4;
pixels.data[pxi] = converted[0];
pixels.data[pxi + 1] = converted[1];
pixels.data[pxi + 2] = converted[2];
pixels.data[pxi + 3] = converted[3];
}
}
return pixels;
}
/**
* Generates a nether portal texture with given dimensions
* @author Evorp
* @param {PortalOptions} options - Generation options
* @returns {Buffer} PNG buffer of the compiled portal atlas
*/
function generatePortalTexture(options = {}) {
if (!options.width || !options.height)
throw new SyntaxError("You need to specify width and height properties!");
options.frameCount ||= 32;
options.noise ??= 0.1;
options.spiralCount ??= 10;
const canvas = createCanvas(options.width, options.height * options.frameCount);
const ctx = canvas.getContext("2d");
for (let frameIndex = 0; frameIndex < options.frameCount; ++frameIndex)
ctx.putImageData(generatePortalFrame(frameIndex, options), 0, frameIndex * options.height);
return canvas.toBuffer("image/png");
}
// sample usage
writeFileSync(
`./nether_portal.png`,
generatePortalTexture({ width: 16, height: 16, noise: 0.1, frameCount: 32, spiralCount: 10 })
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment