@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
827 lines (826 loc) • 31 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyLightingEffect = applyLightingEffect;
exports.applyBorderEffect = applyBorderEffect;
exports.applyOverlayEffect = applyOverlayEffect;
exports.applyColorVariationEffect = applyColorVariationEffect;
exports.applyTilingEffect = applyTilingEffect;
exports.applyTextureEffects = applyTextureEffects;
/**
* TextureEffects
*
* A reusable library of pixel-level texture effects for Minecraft-style textures.
* These effects operate directly on RGBA pixel buffers for maximum performance.
*
* ARCHITECTURE:
* - All effects operate on Uint8Array RGBA pixel buffers (width * height * 4 bytes)
* - Effects are pure functions that modify the pixel buffer in-place
* - Effects can be composed/chained for complex visual results
* - All coordinates use standard image coordinates (0,0 = top-left)
*
* EFFECT CATEGORIES:
* 1. Lighting Effects - Add depth/dimension via light simulation (inset, outset, pillow, ambient_occlusion)
* 2. Border Effects - Add edges/outlines with CSS-like syntax (solid, dashed, worn, highlight)
* 3. Overlay Effects - Add weathering/detail patterns (cracks, scratches, moss, rust, sparkle, veins)
* 4. Color Variation - Modify color distribution (hue_shift, saturation_jitter, value_jitter, palette_snap)
* 5. Tiling Effects - Make textures seamless/patterned (seamless, brick, herringbone, basketweave)
*/
const NoiseGenerationUtilities_1 = require("./NoiseGenerationUtilities");
const NoiseGenerationUtilities_2 = __importDefault(require("./NoiseGenerationUtilities"));
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Get pixel index in RGBA buffer.
*/
function getPixelIndex(x, y, width) {
return (y * width + x) * 4;
}
/**
* Get pixel color from buffer.
*/
function getPixel(pixels, x, y, width) {
const idx = getPixelIndex(x, y, width);
return {
r: pixels[idx],
g: pixels[idx + 1],
b: pixels[idx + 2],
a: pixels[idx + 3],
};
}
/**
* Set pixel color in buffer.
*/
function setPixel(pixels, x, y, width, color) {
const idx = getPixelIndex(x, y, width);
pixels[idx] = color.r;
pixels[idx + 1] = color.g;
pixels[idx + 2] = color.b;
pixels[idx + 3] = color.a;
}
/**
* Blend source color over destination with alpha.
*/
function blendOver(dst, src, opacity = 1) {
const srcAlpha = (src.a / 255) * opacity;
const dstAlpha = dst.a / 255;
const outAlpha = srcAlpha + dstAlpha * (1 - srcAlpha);
if (outAlpha === 0) {
return { r: 0, g: 0, b: 0, a: 0 };
}
return {
r: Math.round((src.r * srcAlpha + dst.r * dstAlpha * (1 - srcAlpha)) / outAlpha),
g: Math.round((src.g * srcAlpha + dst.g * dstAlpha * (1 - srcAlpha)) / outAlpha),
b: Math.round((src.b * srcAlpha + dst.b * dstAlpha * (1 - srcAlpha)) / outAlpha),
a: Math.round(outAlpha * 255),
};
}
/**
* Lighten a color by a factor.
*/
function lightenColor(color, factor) {
return {
r: Math.min(255, Math.round(color.r + (255 - color.r) * factor)),
g: Math.min(255, Math.round(color.g + (255 - color.g) * factor)),
b: Math.min(255, Math.round(color.b + (255 - color.b) * factor)),
a: color.a,
};
}
/**
* Darken a color by a factor.
*/
function darkenColor(color, factor) {
return {
r: Math.max(0, Math.round(color.r * (1 - factor))),
g: Math.max(0, Math.round(color.g * (1 - factor))),
b: Math.max(0, Math.round(color.b * (1 - factor))),
a: color.a,
};
}
/**
* Convert RGB to HSV.
*/
function rgbToHsv(color) {
const r = color.r / 255;
const g = color.g / 255;
const b = color.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
const s = max === 0 ? 0 : d / max;
const v = max;
if (max !== min) {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return { h, s, v };
}
/**
* Convert HSV to RGB.
*/
function hsvToRgb(h, s, v, a = 255) {
let r = 0, g = 0, b = 0;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
a,
};
}
/**
* Calculate color distance for palette snapping.
*/
function colorDistance(c1, c2) {
const dr = c1.r - c2.r;
const dg = c1.g - c2.g;
const db = c1.b - c2.b;
return dr * dr + dg * dg + db * db;
}
// ============================================================================
// LIGHTING EFFECTS
// ============================================================================
/**
* Apply lighting effect to pixel buffer.
* Creates pseudo-3D depth by simulating light hitting a surface.
*/
function applyLightingEffect(pixels, width, height, effect) {
const intensity = effect.intensity ?? 0.3;
const angle = effect.angle ?? 315; // Default: top-left
// Pre-calculate angle components
const angleRad = (angle * Math.PI) / 180;
const lightX = Math.cos(angleRad);
const lightY = -Math.sin(angleRad); // Negative because Y increases downward
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const color = getPixel(pixels, x, y, width);
if (color.a === 0)
continue; // Skip transparent pixels
let lightFactor = 0;
switch (effect.preset) {
case "inset":
// Inset: darken near light source, lighten away from it
{
const nx = (x / (width - 1)) * 2 - 1; // Normalize to -1 to 1
const ny = (y / (height - 1)) * 2 - 1;
lightFactor = -(nx * lightX + ny * lightY) * intensity;
}
break;
case "outset":
// Outset: lighten near light source, darken away from it
{
const nx = (x / (width - 1)) * 2 - 1;
const ny = (y / (height - 1)) * 2 - 1;
lightFactor = (nx * lightX + ny * lightY) * intensity;
}
break;
case "pillow":
// Pillow: darken edges, lighten center
{
const dx = Math.abs((x / (width - 1)) * 2 - 1);
const dy = Math.abs((y / (height - 1)) * 2 - 1);
const edgeDist = Math.max(dx, dy);
lightFactor = (1 - edgeDist * 2) * intensity;
}
break;
case "ambient_occlusion":
// Ambient occlusion: darken corners and edges
{
const dx = Math.min(x, width - 1 - x) / (width / 2);
const dy = Math.min(y, height - 1 - y) / (height / 2);
const cornerDist = Math.min(dx, dy);
lightFactor = (cornerDist - 0.5) * intensity * 2;
}
break;
}
// Apply lighting
let newColor;
if (lightFactor > 0) {
newColor = lightenColor(color, lightFactor);
}
else {
newColor = darkenColor(color, -lightFactor);
}
setPixel(pixels, x, y, width, newColor);
}
}
}
// ============================================================================
// BORDER EFFECTS
// ============================================================================
/**
* Apply border effect to pixel buffer.
* Uses CSS-like syntax for specifying individual or all sides.
*/
function applyBorderEffect(pixels, width, height, effect) {
const rng = new NoiseGenerationUtilities_1.SeededRandom(effect.seed ?? 12345);
// Resolve each side's configuration
const sides = {
top: effect.top ?? effect.all,
right: effect.right ?? effect.all,
bottom: effect.bottom ?? effect.all,
left: effect.left ?? effect.all,
};
// Apply each side
if (sides.top) {
applyBorderSide(pixels, width, height, "top", sides.top, rng);
}
if (sides.right) {
applyBorderSide(pixels, width, height, "right", sides.right, rng);
}
if (sides.bottom) {
applyBorderSide(pixels, width, height, "bottom", sides.bottom, rng);
}
if (sides.left) {
applyBorderSide(pixels, width, height, "left", sides.left, rng);
}
}
/**
* Apply a single border side.
*/
function applyBorderSide(pixels, width, height, side, config, rng) {
const borderWidth = Math.min(config.width ?? 1, 8);
const style = config.style;
// Parse or auto-generate border color
let borderColor;
if (config.color) {
borderColor = NoiseGenerationUtilities_2.default.parseColorInput(config.color);
}
else {
// Sample center pixel and auto-adjust
const centerColor = getPixel(pixels, Math.floor(width / 2), Math.floor(height / 2), width);
if (style === "highlight") {
borderColor = lightenColor(centerColor, 0.4);
}
else {
borderColor = darkenColor(centerColor, 0.3);
}
}
// Determine pixel ranges based on side
let xStart, xEnd, yStart, yEnd;
switch (side) {
case "top":
xStart = 0;
xEnd = width;
yStart = 0;
yEnd = borderWidth;
break;
case "bottom":
xStart = 0;
xEnd = width;
yStart = height - borderWidth;
yEnd = height;
break;
case "left":
xStart = 0;
xEnd = borderWidth;
yStart = 0;
yEnd = height;
break;
case "right":
xStart = width - borderWidth;
xEnd = width;
yStart = 0;
yEnd = height;
break;
}
// Apply border pixels
for (let y = yStart; y < yEnd; y++) {
for (let x = xStart; x < xEnd; x++) {
let shouldDraw = true;
let colorToUse = borderColor;
switch (style) {
case "solid":
// Always draw
break;
case "dashed":
// Draw every other 2-pixel segment
const pos = side === "top" || side === "bottom" ? x : y;
shouldDraw = Math.floor(pos / 2) % 2 === 0;
break;
case "worn":
// Irregular drawing with random gaps
shouldDraw = rng.next() > 0.2;
if (shouldDraw) {
// Add slight color variation
const variation = (rng.next() - 0.5) * 0.2;
if (variation > 0) {
colorToUse = lightenColor(borderColor, variation);
}
else {
colorToUse = darkenColor(borderColor, -variation);
}
}
break;
case "highlight":
// Brighter border with slight gradient
{
const distFromEdge = side === "top"
? y - yStart
: side === "bottom"
? yEnd - 1 - y
: side === "left"
? x - xStart
: xEnd - 1 - x;
const gradient = 1 - distFromEdge / borderWidth;
colorToUse = lightenColor(borderColor, gradient * 0.2);
}
break;
}
if (shouldDraw) {
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, colorToUse, 0.8);
setPixel(pixels, x, y, width, blended);
}
}
}
}
// ============================================================================
// OVERLAY EFFECTS
// ============================================================================
/**
* Apply overlay effect to pixel buffer.
* Adds surface detail without modifying base structure.
*/
function applyOverlayEffect(pixels, width, height, effect) {
const density = effect.density ?? 0.3;
const rng = new NoiseGenerationUtilities_1.SeededRandom(effect.seed ?? 54321);
// Get pattern-specific default color
let overlayColor;
if (effect.color) {
overlayColor = NoiseGenerationUtilities_2.default.parseColorInput(effect.color);
}
else {
switch (effect.pattern) {
case "cracks":
overlayColor = { r: 30, g: 30, b: 30, a: 180 };
break;
case "scratches":
overlayColor = { r: 200, g: 200, b: 200, a: 150 };
break;
case "moss":
overlayColor = { r: 60, g: 120, b: 40, a: 180 };
break;
case "rust":
overlayColor = { r: 180, g: 80, b: 30, a: 180 };
break;
case "sparkle":
overlayColor = { r: 255, g: 255, b: 255, a: 255 };
break;
case "veins":
overlayColor = { r: 50, g: 50, b: 60, a: 160 };
break;
default:
overlayColor = { r: 128, g: 128, b: 128, a: 128 };
}
}
switch (effect.pattern) {
case "cracks":
applyCracksOverlay(pixels, width, height, overlayColor, density, rng);
break;
case "scratches":
applyScratchesOverlay(pixels, width, height, overlayColor, density, rng);
break;
case "moss":
applyMossOverlay(pixels, width, height, overlayColor, density, rng);
break;
case "rust":
applyRustOverlay(pixels, width, height, overlayColor, density, rng);
break;
case "sparkle":
applySparkleOverlay(pixels, width, height, overlayColor, density, rng);
break;
case "veins":
applyVeinsOverlay(pixels, width, height, overlayColor, density, rng);
break;
}
}
/**
* Apply cracks overlay - dark lines suggesting fractures.
*/
function applyCracksOverlay(pixels, width, height, color, density, rng) {
const numCracks = Math.floor(density * 5) + 1;
for (let i = 0; i < numCracks; i++) {
// Start point
let x = rng.nextInt(0, width - 1);
let y = rng.nextInt(0, height - 1);
// Crack length
const length = rng.nextInt(3, Math.max(width, height) / 2);
for (let j = 0; j < length; j++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, color, 0.7);
setPixel(pixels, x, y, width, blended);
}
// Random walk with preference for downward/diagonal
const dir = rng.next();
if (dir < 0.4) {
y += 1;
}
else if (dir < 0.6) {
x += rng.next() < 0.5 ? -1 : 1;
}
else if (dir < 0.8) {
x += 1;
y += 1;
}
else {
x -= 1;
y += 1;
}
}
}
}
/**
* Apply scratches overlay - light linear marks.
*/
function applyScratchesOverlay(pixels, width, height, color, density, rng) {
const numScratches = Math.floor(density * 8) + 1;
for (let i = 0; i < numScratches; i++) {
// Start point
const x1 = rng.nextInt(0, width - 1);
const y1 = rng.nextInt(0, height - 1);
// End point - mostly horizontal or diagonal scratches
const angle = (rng.next() - 0.5) * Math.PI * 0.5; // -45 to +45 degrees
const length = rng.nextInt(2, width / 3);
const x2 = Math.round(x1 + Math.cos(angle) * length);
const y2 = Math.round(y1 + Math.sin(angle) * length);
// Draw line using Bresenham's algorithm
drawLine(pixels, width, height, x1, y1, x2, y2, color, 0.5);
}
}
/**
* Draw a line using Bresenham's algorithm.
*/
function drawLine(pixels, width, height, x1, y1, x2, y2, color, opacity) {
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
const sx = x1 < x2 ? 1 : -1;
const sy = y1 < y2 ? 1 : -1;
let err = dx - dy;
let x = x1;
let y = y1;
while (true) {
if (x >= 0 && x < width && y >= 0 && y < height) {
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, color, opacity);
setPixel(pixels, x, y, width, blended);
}
if (x === x2 && y === y2)
break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
}
/**
* Apply moss overlay - green organic patches.
*/
function applyMossOverlay(pixels, width, height, color, density, rng) {
const numPatches = Math.floor(density * 6) + 1;
for (let i = 0; i < numPatches; i++) {
const cx = rng.nextInt(0, width - 1);
const cy = rng.nextInt(0, height - 1);
const radius = rng.nextInt(1, 3);
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const x = cx + dx;
const y = cy + dy;
if (x >= 0 && x < width && y >= 0 && y < height) {
// Irregular shape with random fill
if (rng.next() < 0.6 && dx * dx + dy * dy <= radius * radius) {
// Vary the green color slightly
const variedColor = {
r: color.r + rng.nextInt(-10, 10),
g: Math.min(255, color.g + rng.nextInt(-20, 20)),
b: color.b + rng.nextInt(-10, 10),
a: color.a,
};
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, variedColor, 0.6);
setPixel(pixels, x, y, width, blended);
}
}
}
}
}
}
/**
* Apply rust overlay - orange/brown oxidation spots.
*/
function applyRustOverlay(pixels, width, height, color, density, rng) {
const numSpots = Math.floor(density * 10) + 2;
for (let i = 0; i < numSpots; i++) {
const cx = rng.nextInt(0, width - 1);
const cy = rng.nextInt(0, height - 1);
const size = rng.nextInt(1, 2);
for (let dy = -size; dy <= size; dy++) {
for (let dx = -size; dx <= size; dx++) {
const x = cx + dx;
const y = cy + dy;
if (x >= 0 && x < width && y >= 0 && y < height && rng.next() < 0.7) {
// Vary the rust color
const variedColor = {
r: Math.min(255, color.r + rng.nextInt(-20, 20)),
g: Math.max(0, color.g + rng.nextInt(-20, 10)),
b: Math.max(0, color.b + rng.nextInt(-10, 10)),
a: color.a,
};
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, variedColor, 0.5);
setPixel(pixels, x, y, width, blended);
}
}
}
}
}
/**
* Apply sparkle overlay - bright highlight dots.
*/
function applySparkleOverlay(pixels, width, height, color, density, rng) {
const numSparkles = Math.floor(density * width * height * 0.02) + 1;
for (let i = 0; i < numSparkles; i++) {
const x = rng.nextInt(0, width - 1);
const y = rng.nextInt(0, height - 1);
// Bright sparkle with slight color variation
const brightness = 200 + rng.nextInt(0, 55);
const sparkleColor = {
r: brightness,
g: brightness,
b: Math.min(255, brightness + rng.nextInt(-20, 20)),
a: 255,
};
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, sparkleColor, 0.8);
setPixel(pixels, x, y, width, blended);
}
}
/**
* Apply veins overlay - dark branching lines.
*/
function applyVeinsOverlay(pixels, width, height, color, density, rng) {
const numVeins = Math.floor(density * 3) + 1;
for (let i = 0; i < numVeins; i++) {
// Start from edge
let x = rng.next() < 0.5 ? 0 : width - 1;
let y = rng.nextInt(0, height - 1);
const direction = x === 0 ? 1 : -1;
const length = rng.nextInt(width / 3, width);
for (let j = 0; j < length; j++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
const existingColor = getPixel(pixels, x, y, width);
const blended = blendOver(existingColor, color, 0.6);
setPixel(pixels, x, y, width, blended);
}
// Move with branching tendency
x += direction;
if (rng.next() < 0.3) {
y += rng.next() < 0.5 ? -1 : 1;
}
// Occasional branch
if (rng.next() < 0.1) {
const branchY = y;
let branchX = x;
const branchDir = rng.next() < 0.5 ? -1 : 1;
for (let k = 0; k < rng.nextInt(2, 5); k++) {
const by = branchY + k * branchDir;
if (branchX >= 0 && branchX < width && by >= 0 && by < height) {
const existingColor = getPixel(pixels, branchX, by, width);
const blended = blendOver(existingColor, color, 0.4);
setPixel(pixels, branchX, by, width, blended);
}
branchX += direction;
}
}
}
}
}
// ============================================================================
// COLOR VARIATION EFFECTS
// ============================================================================
/**
* Apply color variation effect to pixel buffer.
*/
function applyColorVariationEffect(pixels, width, height, effect) {
const amount = effect.amount ?? 0.1;
const rng = new NoiseGenerationUtilities_1.SeededRandom(effect.seed ?? 98765);
// Parse palette colors if provided
let paletteColors = [];
if (effect.palette) {
paletteColors = effect.palette.map((c) => NoiseGenerationUtilities_2.default.parseColorInput(c));
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const color = getPixel(pixels, x, y, width);
if (color.a === 0)
continue;
let newColor;
switch (effect.mode) {
case "hue_shift":
{
const hsv = rgbToHsv(color);
hsv.h = (hsv.h + (rng.next() - 0.5) * amount) % 1;
if (hsv.h < 0)
hsv.h += 1;
newColor = hsvToRgb(hsv.h, hsv.s, hsv.v, color.a);
}
break;
case "saturation_jitter":
{
const hsv = rgbToHsv(color);
hsv.s = Math.max(0, Math.min(1, hsv.s + (rng.next() - 0.5) * amount * 2));
newColor = hsvToRgb(hsv.h, hsv.s, hsv.v, color.a);
}
break;
case "value_jitter":
{
const hsv = rgbToHsv(color);
hsv.v = Math.max(0, Math.min(1, hsv.v + (rng.next() - 0.5) * amount * 2));
newColor = hsvToRgb(hsv.h, hsv.s, hsv.v, color.a);
}
break;
case "palette_snap":
if (paletteColors.length > 0) {
// Find nearest palette color
let nearest = paletteColors[0];
let nearestDist = colorDistance(color, nearest);
for (let i = 1; i < paletteColors.length; i++) {
const dist = colorDistance(color, paletteColors[i]);
if (dist < nearestDist) {
nearest = paletteColors[i];
nearestDist = dist;
}
}
newColor = { ...nearest, a: color.a };
}
else {
newColor = color;
}
break;
default:
newColor = color;
}
setPixel(pixels, x, y, width, newColor);
}
}
}
// ============================================================================
// TILING EFFECTS
// ============================================================================
/**
* Apply tiling effect to pixel buffer.
* Makes textures seamless or applies tiling patterns.
*/
function applyTilingEffect(pixels, width, height, effect) {
// Apply seamless blending if requested
if (effect.seamless) {
makeSeamless(pixels, width, height);
}
// Note: Pattern tiling would typically be applied during texture generation
// rather than as a post-process. This could be expanded in the future.
}
/**
* Make texture seamless by blending edges.
*/
function makeSeamless(pixels, width, height) {
const blendWidth = Math.max(1, Math.floor(Math.min(width, height) / 4));
// Create a copy for reading while we write
const original = new Uint8Array(pixels);
// Blend horizontal seam (left-right)
for (let y = 0; y < height; y++) {
for (let i = 0; i < blendWidth; i++) {
const t = i / blendWidth;
// Left edge blends with right side
const leftIdx = getPixelIndex(i, y, width);
const rightMirrorIdx = getPixelIndex(width - 1 - i, y, width);
// Right edge blends with left side
const rightIdx = getPixelIndex(width - 1 - i, y, width);
const leftMirrorIdx = getPixelIndex(i, y, width);
// Blend colors
for (let c = 0; c < 4; c++) {
pixels[leftIdx + c] = Math.round(original[leftIdx + c] * t + original[rightMirrorIdx + c] * (1 - t));
pixels[rightIdx + c] = Math.round(original[rightIdx + c] * t + original[leftMirrorIdx + c] * (1 - t));
}
}
}
// Update original for vertical pass
original.set(pixels);
// Blend vertical seam (top-bottom)
for (let x = 0; x < width; x++) {
for (let i = 0; i < blendWidth; i++) {
const t = i / blendWidth;
// Top edge blends with bottom side
const topIdx = getPixelIndex(x, i, width);
const bottomMirrorIdx = getPixelIndex(x, height - 1 - i, width);
// Bottom edge blends with top side
const bottomIdx = getPixelIndex(x, height - 1 - i, width);
const topMirrorIdx = getPixelIndex(x, i, width);
// Blend colors
for (let c = 0; c < 4; c++) {
pixels[topIdx + c] = Math.round(original[topIdx + c] * t + original[bottomMirrorIdx + c] * (1 - t));
pixels[bottomIdx + c] = Math.round(original[bottomIdx + c] * t + original[topMirrorIdx + c] * (1 - t));
}
}
}
}
// ============================================================================
// MAIN EFFECT APPLICATION
// ============================================================================
/**
* Apply all texture effects to a pixel buffer in the correct order.
*
* Order of application:
* 1. Color variation (modifies base colors)
* 2. Lighting (adds depth based on position)
* 3. Overlays (adds surface detail)
* 4. Borders (adds edge definition)
* 5. Tiling (makes seamless)
*
* @param pixels RGBA pixel buffer to modify in-place
* @param width Buffer width
* @param height Buffer height
* @param effects Effects configuration
*/
function applyTextureEffects(pixels, width, height, effects) {
// 1. Color variation first (affects base colors for other effects)
if (effects.colorVariation) {
applyColorVariationEffect(pixels, width, height, effects.colorVariation);
}
// 2. Lighting (position-based shading)
if (effects.lighting) {
applyLightingEffect(pixels, width, height, effects.lighting);
}
// 3. Overlays (surface detail)
if (effects.overlay) {
const overlays = Array.isArray(effects.overlay) ? effects.overlay : [effects.overlay];
for (const overlay of overlays) {
applyOverlayEffect(pixels, width, height, overlay);
}
}
// 4. Borders (edge definition) - after overlays so they're on top
if (effects.border) {
applyBorderEffect(pixels, width, height, effects.border);
}
// 5. Tiling (seamless edges) - last to blend everything
if (effects.tiling) {
applyTilingEffect(pixels, width, height, effects.tiling);
}
}