Last active
July 16, 2024 18:49
-
-
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).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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