@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
499 lines (498 loc) • 23.5 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SeededRandom = exports.NoiseGenerationUtilities = void 0;
const NoiseGenerationUtilities_1 = __importStar(require("./NoiseGenerationUtilities"));
exports.NoiseGenerationUtilities = NoiseGenerationUtilities_1.default;
Object.defineProperty(exports, "SeededRandom", { enumerable: true, get: function () { return NoiseGenerationUtilities_1.SeededRandom; } });
const TextureEffects_1 = require("./TextureEffects");
/**
* Textured rectangle generator for Minecraft-style procedural textures.
*/
class TexturedRectangleGenerator {
/**
* Maps new TexturedRectangleType to legacy NoisePatternType for internal processing.
*/
static texturedRectangleTypeToPattern(type) {
switch (type) {
case "none":
return "none";
case "solid":
return "solid";
case "random_noise":
return "random";
case "dither_noise":
return "dither";
case "perlin_noise":
return "perlin";
case "stipple_noise":
return "stipple";
case "gradient":
return "gradient";
default:
return "random";
}
}
/**
* Generate an SVG string from a textured rectangle configuration.
* This is the primary API for the new unified texture format.
*
* @param config Textured rectangle configuration
* @param width Width of the texture in pixels
* @param height Height of the texture in pixels
* @param contextString Optional context string for seed generation
* @returns SVG string with the texture pattern
*/
static generateTexturedRectangleSvg(config, width, height, contextString) {
// Parse colors
const colors = (config.colors || []).map((c) => NoiseGenerationUtilities_1.default.parseColorInput(c));
if (colors.length === 0) {
// Default to white if no colors provided (ignored for "none")
colors.push({ r: 255, g: 255, b: 255, a: 255 });
}
const pattern = this.texturedRectangleTypeToPattern(config.type);
// Handle "none" - return an empty transparent SVG
if (pattern === "none") {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"></svg>`;
}
// Handle solid color - just return a simple rect
if (pattern === "solid") {
const color = colors[0];
const colorStr = color.a === 255
? `rgb(${color.r},${color.g},${color.b})`
: `rgba(${color.r},${color.g},${color.b},${(color.a / 255).toFixed(3)})`;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" fill="${colorStr}"/></svg>`;
}
// Determine seed
const seed = config.seed ?? NoiseGenerationUtilities_1.default.hashString(contextString || `texture-${Date.now()}`);
const rng = new NoiseGenerationUtilities_1.SeededRandom(seed);
// Get parameters
const factor = Math.max(0, Math.min(1, config.factor ?? 0.2));
const pixelSize = Math.max(1, config.pixelSize ?? 1);
const scale = config.scale ?? 4;
// Calculate grid dimensions
const gridWidth = Math.ceil(width / pixelSize);
const gridHeight = Math.ceil(height / pixelSize);
// Generate the noise grid
const colorGrid = NoiseGenerationUtilities_1.default.generateNoiseGrid(pattern, colors, factor, gridWidth, gridHeight, rng, scale);
// Convert to SVG rects
return this.gridToSvg(colorGrid, width, height, pixelSize);
}
/**
* Generate an SVG string containing the noise texture.
* @deprecated Use generateTexturedRectangleSvg with IMcpTexturedRectangle instead.
*
* @param config Noise configuration with colors, pattern, and parameters
* @param width Width of the texture in pixels
* @param height Height of the texture in pixels
* @param contextString Optional context string for seed generation (e.g., "textureId:wood")
* @returns SVG string with rect elements forming the noise pattern
*/
static generateNoiseSvg(config, width, height, contextString) {
// Parse colors
const colors = config.colors.map((c) => NoiseGenerationUtilities_1.default.parseColorInput(c));
if (colors.length === 0) {
// Default to white if no colors provided
colors.push({ r: 255, g: 255, b: 255, a: 255 });
}
// Determine seed
const seed = config.seed ?? NoiseGenerationUtilities_1.default.hashString(contextString || `noise-${Date.now()}`);
const rng = new NoiseGenerationUtilities_1.SeededRandom(seed);
// Get parameters
const pattern = config.pattern || "random";
// Default factor of 0.2 provides subtle noise without being too grainy
const factor = Math.max(0, Math.min(1, config.factor ?? 0.2));
const pixelSize = Math.max(1, config.pixelSize ?? 1);
const scale = config.scale ?? 4;
// Calculate grid dimensions
const gridWidth = Math.ceil(width / pixelSize);
const gridHeight = Math.ceil(height / pixelSize);
// Generate the noise grid
const colorGrid = NoiseGenerationUtilities_1.default.generateNoiseGrid(pattern, colors, factor, gridWidth, gridHeight, rng, scale);
// Convert to SVG rects
return this.gridToSvg(colorGrid, width, height, pixelSize);
}
/**
* Convert color grid to SVG with rect elements
*/
static gridToSvg(grid, width, height, pixelSize) {
const rects = [];
// Optimize by grouping adjacent same-color pixels horizontally
for (let y = 0; y < grid.length; y++) {
const row = grid[y];
let runStart = 0;
let runColor = row[0];
for (let x = 1; x <= row.length; x++) {
const currentColor = x < row.length ? row[x] : null;
// Check if run ends
if (!currentColor || !NoiseGenerationUtilities_1.default.colorsEqual(currentColor, runColor)) {
// Output the run
const rectX = runStart * pixelSize;
const rectY = y * pixelSize;
const rectWidth = (x - runStart) * pixelSize;
const rectHeight = pixelSize;
// Clamp to texture bounds
const clampedWidth = Math.min(rectWidth, width - rectX);
const clampedHeight = Math.min(rectHeight, height - rectY);
if (clampedWidth > 0 && clampedHeight > 0) {
const colorStr = NoiseGenerationUtilities_1.default.colorToHex(runColor);
rects.push(`<rect x="${rectX}" y="${rectY}" width="${clampedWidth}" height="${clampedHeight}" fill="${colorStr}"/>`);
}
// Start new run
if (currentColor) {
runStart = x;
runColor = currentColor;
}
}
}
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${rects.join("")}</svg>`;
}
/**
* Combine noise SVG with optional overlay SVG.
* The noise forms the background, and the overlay is drawn on top.
*
* @param noiseSvg The noise pattern SVG
* @param overlaySvg Optional SVG to draw on top of the noise
* @param width Texture width
* @param height Texture height
* @returns Combined SVG string
*/
static combineWithOverlay(noiseSvg, overlaySvg, width, height) {
if (!overlaySvg) {
return noiseSvg;
}
// Extract inner content from both SVGs
const noiseInner = this.extractSvgInner(noiseSvg);
const overlayInner = this.extractSvgInner(overlaySvg);
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${noiseInner}${overlayInner}</svg>`;
}
/**
* Extract inner content from SVG string (removing outer svg tags)
*/
static extractSvgInner(svg) {
const match = svg.match(/<svg[^>]*>([\s\S]*)<\/svg>/i);
return match ? match[1] : svg;
}
/**
* Generate noise texture as RGBA pixel data (Uint8Array).
* This bypasses SVG generation entirely for better performance.
* @deprecated Use generateTexturedRectanglePixels with IMcpTexturedRectangle instead.
*
* @param config Noise configuration with colors, pattern, and parameters
* @param width Width of the texture in pixels
* @param height Height of the texture in pixels
* @param contextString Optional context string for seed generation
* @returns RGBA pixel data as Uint8Array (width * height * 4 bytes)
*/
static generateNoisePixels(config, width, height, contextString) {
// Parse colors
const colors = config.colors.map((c) => NoiseGenerationUtilities_1.default.parseColorInput(c));
if (colors.length === 0) {
// Default to white if no colors provided
colors.push({ r: 255, g: 255, b: 255, a: 255 });
}
// Determine seed
const seed = config.seed ?? NoiseGenerationUtilities_1.default.hashString(contextString || `noise-${Date.now()}`);
const rng = new NoiseGenerationUtilities_1.SeededRandom(seed);
// Get parameters
const pattern = config.pattern || "random";
const factor = Math.max(0, Math.min(1, config.factor ?? 0.2));
const pixelSize = Math.max(1, config.pixelSize ?? 1);
const scale = config.scale ?? 4;
// Calculate grid dimensions
const gridWidth = Math.ceil(width / pixelSize);
const gridHeight = Math.ceil(height / pixelSize);
// Generate the noise grid
const colorGrid = NoiseGenerationUtilities_1.default.generateNoiseGrid(pattern, colors, factor, gridWidth, gridHeight, rng, scale);
// Convert grid to RGBA pixel data
return this.gridToPixels(colorGrid, width, height, pixelSize);
}
/**
* Generate RGBA pixel data from a textured rectangle configuration.
* This is the primary API for the new unified texture format.
*
* @param config Textured rectangle configuration
* @param width Width of the texture in pixels
* @param height Height of the texture in pixels
* @param contextString Optional context string for seed generation
* @returns RGBA pixel data as Uint8Array (width * height * 4 bytes)
*/
static generatePixels(config, width, height, contextString) {
// Parse colors
const colors = (config.colors || []).map((c) => NoiseGenerationUtilities_1.default.parseColorInput(c));
if (colors.length === 0) {
// Default to white if no colors provided (ignored for "none")
colors.push({ r: 255, g: 255, b: 255, a: 255 });
}
const pattern = this.texturedRectangleTypeToPattern(config.type);
let pixels;
if (pattern === "none") {
// Fully transparent background. Uint8Array defaults to zero, so every RGBA
// byte — including the alpha channel — is 0, producing a fully transparent image.
pixels = new Uint8Array(width * height * 4);
}
else if (pattern === "solid") {
// Handle solid color - fill with first color
pixels = new Uint8Array(width * height * 4);
const color = colors[0];
for (let i = 0; i < width * height; i++) {
const idx = i * 4;
pixels[idx] = color.r;
pixels[idx + 1] = color.g;
pixels[idx + 2] = color.b;
pixels[idx + 3] = color.a;
}
}
else {
// Determine seed
const seed = config.seed ?? NoiseGenerationUtilities_1.default.hashString(contextString || `texture-${Date.now()}`);
const rng = new NoiseGenerationUtilities_1.SeededRandom(seed);
// Get parameters
const factor = Math.max(0, Math.min(1, config.factor ?? 0.2));
const pixelSize = Math.max(1, config.pixelSize ?? 1);
const scale = config.scale ?? 4;
// Calculate grid dimensions
const gridWidth = Math.ceil(width / pixelSize);
const gridHeight = Math.ceil(height / pixelSize);
// Generate the noise grid
const colorGrid = NoiseGenerationUtilities_1.default.generateNoiseGrid(pattern, colors, factor, gridWidth, gridHeight, rng, scale);
// Convert grid to RGBA pixel data
pixels = this.gridToPixels(colorGrid, width, height, pixelSize);
}
// Apply post-processing effects if specified
if (config.effects) {
(0, TextureEffects_1.applyTextureEffects)(pixels, width, height, config.effects);
}
return pixels;
}
/**
* Convert color grid to RGBA pixel data
*/
static gridToPixels(grid, width, height, pixelSize) {
const gridWidth = grid[0]?.length || 0;
const gridHeight = grid.length;
const pixels = new Uint8Array(width * height * 4);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Map pixel to grid cell
const gridX = Math.min(Math.floor(x / pixelSize), gridWidth - 1);
const gridY = Math.min(Math.floor(y / pixelSize), gridHeight - 1);
const color = grid[gridY][gridX];
// Write RGBA values
const idx = (y * width + x) * 4;
pixels[idx] = color.r;
pixels[idx + 1] = color.g;
pixels[idx + 2] = color.b;
pixels[idx + 3] = color.a;
}
}
return pixels;
}
/**
* Parse an IMcpPixelColor to a ParsedColor (RGBA 0-255).
*/
static parsePixelColor(color) {
// If hex is provided, parse it
if (color.hex) {
return NoiseGenerationUtilities_1.default.parseColorInput(color.hex);
}
// Otherwise use RGBA values
return {
r: Math.max(0, Math.min(255, color.r || 0)),
g: Math.max(0, Math.min(255, color.g || 0)),
b: Math.max(0, Math.min(255, color.b || 0)),
a: color.a !== undefined ? Math.max(0, Math.min(255, color.a)) : 255,
};
}
/**
* Apply pixel art overlay to an existing RGBA pixel buffer.
* This is the core pixel art rendering method - operates directly on pixel data
* for maximum performance (no SVG generation).
*
* @param pixels Existing RGBA pixel buffer to modify in-place
* @param width Width of the pixel buffer (face texture width in pixels)
* @param height Height of the pixel buffer (face texture height in pixels)
* @param pixelArt Pixel art configuration to apply
* @param pixelsPerUnit Pixels per Minecraft unit (for "unit" scaleMode). Default: 2
*/
static applyPixelArt(pixels, width, height, pixelArt, pixelsPerUnit = 2) {
const scaleMode = pixelArt.scaleMode || "unit";
const lines = pixelArt.lines;
const palette = pixelArt.palette;
// Calculate pixel art dimensions
const artHeight = lines.length;
let artWidth = 0;
for (const line of lines) {
artWidth = Math.max(artWidth, line.length);
}
if (artWidth === 0 || artHeight === 0) {
return; // Empty pixel art
}
// Pre-parse palette colors for performance
const parsedPalette = {};
for (const char in palette) {
parsedPalette[char] = this.parsePixelColor(palette[char]);
}
// Calculate scale factor and offset based on scaleMode
let scale;
let offsetX;
let offsetY;
switch (scaleMode) {
case "exact":
// Each character = 1 pixel, x/y in pixels
scale = 1;
offsetX = pixelArt.x || 0;
offsetY = pixelArt.y || 0;
break;
case "cover":
// Scale to fill the entire texture, ignoring x/y
const scaleX = width / artWidth;
const scaleY = height / artHeight;
scale = Math.min(scaleX, scaleY); // Use min to maintain aspect ratio and cover
// For true cover, we'd use max, but min prevents overflow
// Center the art if it doesn't fill completely
offsetX = Math.floor((width - artWidth * scale) / 2);
offsetY = Math.floor((height - artHeight * scale) / 2);
break;
case "unit":
default:
// Each character = 1 Minecraft unit = pixelsPerUnit pixels
// x/y in Minecraft units
scale = pixelsPerUnit;
offsetX = (pixelArt.x || 0) * pixelsPerUnit;
offsetY = (pixelArt.y || 0) * pixelsPerUnit;
break;
}
// Process each character in the pixel art
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx];
for (let charIdx = 0; charIdx < line.length; charIdx++) {
const char = line[charIdx];
// Space is always transparent (skip drawing)
if (char === " ")
continue;
// Look up color in palette
const color = parsedPalette[char];
if (!color) {
// Character not in palette - skip silently
continue;
}
// Calculate the pixel region for this character
const startX = Math.floor(offsetX + charIdx * scale);
const startY = Math.floor(offsetY + lineIdx * scale);
const endX = Math.floor(offsetX + (charIdx + 1) * scale);
const endY = Math.floor(offsetY + (lineIdx + 1) * scale);
// Fill the scaled pixel region
for (let pixelY = startY; pixelY < endY; pixelY++) {
// Skip if outside texture bounds
if (pixelY < 0 || pixelY >= height)
continue;
for (let pixelX = startX; pixelX < endX; pixelX++) {
// Skip if outside texture bounds
if (pixelX < 0 || pixelX >= width)
continue;
// Calculate pixel index in buffer
const idx = (pixelY * width + pixelX) * 4;
// Alpha blend the pixel
if (color.a === 255) {
// Fully opaque - direct write
pixels[idx] = color.r;
pixels[idx + 1] = color.g;
pixels[idx + 2] = color.b;
pixels[idx + 3] = 255;
}
else if (color.a > 0) {
// Partial transparency - alpha blend
const srcAlpha = color.a / 255;
const dstAlpha = pixels[idx + 3] / 255;
const outAlpha = srcAlpha + dstAlpha * (1 - srcAlpha);
if (outAlpha > 0) {
pixels[idx] = Math.round((color.r * srcAlpha + pixels[idx] * dstAlpha * (1 - srcAlpha)) / outAlpha);
pixels[idx + 1] = Math.round((color.g * srcAlpha + pixels[idx + 1] * dstAlpha * (1 - srcAlpha)) / outAlpha);
pixels[idx + 2] = Math.round((color.b * srcAlpha + pixels[idx + 2] * dstAlpha * (1 - srcAlpha)) / outAlpha);
pixels[idx + 3] = Math.round(outAlpha * 255);
}
}
// If alpha is 0, don't draw anything
}
}
}
}
}
/**
* Apply multiple pixel art overlays in order.
* Each layer is rendered on top of the previous.
*
* @param pixels Existing RGBA pixel buffer to modify in-place
* @param width Width of the pixel buffer
* @param height Height of the pixel buffer
* @param pixelArtLayers Array of pixel art configurations to apply
* @param pixelsPerUnit Pixels per Minecraft unit (for "unit" scaleMode). Default: 2
*/
static applyPixelArtLayers(pixels, width, height, pixelArtLayers, pixelsPerUnit = 2) {
for (const layer of pixelArtLayers) {
this.applyPixelArt(pixels, width, height, layer, pixelsPerUnit);
}
}
/**
* Generate pixel art as standalone RGBA pixel data.
* Creates a transparent buffer and applies the pixel art to it.
*
* @param pixelArt Pixel art configuration
* @returns Object with pixels (RGBA Uint8Array), width, and height
*/
static generatePixelArtPixels(pixelArt) {
// Calculate dimensions from lines
const height = pixelArt.lines.length;
let width = 0;
for (const line of pixelArt.lines) {
width = Math.max(width, line.length);
}
// Handle empty pixel art
if (width === 0 || height === 0) {
return { pixels: new Uint8Array(0), width: 0, height: 0 };
}
// Create transparent buffer
const pixels = new Uint8Array(width * height * 4);
// Buffer is already zeroed (fully transparent)
// Apply the pixel art
this.applyPixelArt(pixels, width, height, pixelArt);
return { pixels, width, height };
}
}
exports.default = TexturedRectangleGenerator;