@raven-js/cortex
Version:
Zero-dependency machine learning, AI, and data processing library for modern JavaScript
204 lines (188 loc) • 7.12 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file Main rotation functionality for RGBA pixel data.
*
* This module provides the primary rotatePixels function that orchestrates
* all rotation operations, automatically choosing the optimal algorithm
* based on the rotation angle and quality requirements.
*
* @example
* // Rotate 90° (uses fast quadrant rotation)
* const result = rotatePixels(pixels, 800, 600, 90);
* console.log(`Rotated: ${result.width}×${result.height}`);
*
* // Rotate 45° with bilinear interpolation
* const result = rotatePixels(pixels, 800, 600, 45, "bilinear");
*
* // Rotate with custom fill color
* const result = rotatePixels(pixels, 800, 600, 30, "bicubic", [255, 255, 255, 255]);
*/
import { rotateQuadrant } from "./rotate-90.js";
import { rotateBicubic, rotateBilinear, rotateLanczos, rotateNearest } from "./rotate-arbitrary.js";
import {
calculateRotatedDimensions,
isIdentityRotation,
isQuadrantRotation,
normalizeAngle,
validateRotationParameters,
} from "./utils.js";
/**
* Rotates RGBA pixel data by specified angle using optimal algorithm.
*
* This function automatically chooses the best rotation method:
* - Identity rotations (0°, 360°, etc.) return a copy
* - 90° rotations (90°, 180°, 270°) use fast lossless algorithms
* - Arbitrary angles use interpolation-based rotation
*
* @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel)
* @param {number} srcWidth - Source image width in pixels
* @param {number} srcHeight - Source image height in pixels
* @param {number} degrees - Rotation angle in degrees (positive = clockwise)
* @param {string} [algorithm="bilinear"] - Interpolation algorithm for arbitrary angles
* @param {[number, number, number, number]} [fillColor=[0, 0, 0, 0]] - RGBA fill color for empty areas
* @returns {{pixels: Uint8Array, width: number, height: number}} Rotated image data
* @throws {Error} If parameters are invalid
*
* @example
* // Fast 90° rotation
* const result = rotatePixels(pixels, 800, 600, 90);
*
* // High-quality arbitrary rotation
* const result = rotatePixels(pixels, 800, 600, 45, "lanczos", [255, 255, 255, 255]);
*/
export function rotatePixels(pixels, srcWidth, srcHeight, degrees, algorithm = "bilinear", fillColor = [0, 0, 0, 0]) {
// Validate all parameters
validateRotationParameters(pixels, srcWidth, srcHeight, degrees);
// Validate algorithm
const validAlgorithms = ["nearest", "bilinear", "bicubic", "lanczos"];
if (!validAlgorithms.includes(algorithm)) {
throw new Error(`Invalid algorithm: ${algorithm}. Must be one of: ${validAlgorithms.join(", ")}`);
}
// Validate fill color
if (!Array.isArray(fillColor) || fillColor.length !== 4) {
throw new Error("fillColor must be an array of 4 RGBA values [0-255]");
}
for (let i = 0; i < 4; i++) {
if (!Number.isInteger(fillColor[i]) || fillColor[i] < 0 || fillColor[i] > 255) {
throw new Error(`fillColor[${i}] must be an integer between 0 and 255`);
}
}
// Normalize angle to 0-360 range
const normalizedDegrees = normalizeAngle(degrees);
// Handle identity rotation (no change)
if (isIdentityRotation(normalizedDegrees)) {
return {
pixels: new Uint8Array(pixels),
width: srcWidth,
height: srcHeight,
};
}
// Handle 90° rotations with fast algorithms
if (isQuadrantRotation(normalizedDegrees)) {
return rotateQuadrant(pixels, srcWidth, srcHeight, normalizedDegrees);
}
// Handle arbitrary angles with interpolation
switch (algorithm) {
case "nearest":
return rotateNearest(pixels, srcWidth, srcHeight, normalizedDegrees, fillColor);
case "bilinear":
return rotateBilinear(pixels, srcWidth, srcHeight, normalizedDegrees, fillColor);
case "bicubic":
return rotateBicubic(pixels, srcWidth, srcHeight, normalizedDegrees, fillColor);
case "lanczos":
return rotateLanczos(pixels, srcWidth, srcHeight, normalizedDegrees, fillColor);
default:
throw new Error(`Unsupported algorithm: ${algorithm}`);
}
}
/**
* Recommends the optimal rotation algorithm based on angle and quality requirements.
*
* @param {number} degrees - Rotation angle in degrees
* @param {string} [quality="balanced"] - Quality preference: "fast", "balanced", "high"
* @returns {string} Recommended algorithm name
*
* @example
* const algorithm = recommendRotationAlgorithm(45, "high"); // "lanczos"
* const algorithm = recommendRotationAlgorithm(90, "fast"); // "quadrant" (handled automatically)
*/
export function recommendRotationAlgorithm(degrees, quality = "balanced") {
const normalized = normalizeAngle(degrees);
// 90° rotations are always optimal
if (isQuadrantRotation(normalized)) {
return "quadrant"; // Note: this is handled automatically by rotatePixels
}
// For arbitrary angles, recommend based on quality preference
switch (quality) {
case "fast":
return "nearest";
case "balanced":
return "bilinear";
case "high":
return "lanczos";
default:
return "bilinear";
}
}
/**
* Gets information about a rotation operation without performing it.
* Useful for validation and UI feedback.
*
* @param {number} srcWidth - Source image width in pixels
* @param {number} srcHeight - Source image height in pixels
* @param {number} degrees - Rotation angle in degrees
* @returns {{
* normalizedAngle: number,
* isIdentity: boolean,
* isQuadrant: boolean,
* outputDimensions: {width: number, height: number},
* recommendedAlgorithm: string,
* outputSize: number,
* isValid: boolean
* }} Rotation operation information
*/
export function getRotationInfo(srcWidth, srcHeight, degrees) {
try {
// Basic validation
if (!Number.isInteger(srcWidth) || srcWidth <= 0) {
throw new Error("Invalid source width");
}
if (!Number.isInteger(srcHeight) || srcHeight <= 0) {
throw new Error("Invalid source height");
}
if (typeof degrees !== "number" || !Number.isFinite(degrees)) {
throw new Error("Invalid rotation angle");
}
const normalizedAngle = normalizeAngle(degrees);
const isIdentity = isIdentityRotation(normalizedAngle);
const isQuadrant = isQuadrantRotation(normalizedAngle);
const outputDimensions = calculateRotatedDimensions(srcWidth, srcHeight, normalizedAngle);
const recommendedAlgorithm = recommendRotationAlgorithm(normalizedAngle, "balanced");
const outputSize = outputDimensions.width * outputDimensions.height * 4;
return {
normalizedAngle,
isIdentity,
isQuadrant,
outputDimensions,
recommendedAlgorithm,
outputSize,
isValid: true,
};
} catch (_error) {
return {
normalizedAngle: 0,
isIdentity: false,
isQuadrant: false,
outputDimensions: { width: 0, height: 0 },
recommendedAlgorithm: "bilinear",
outputSize: 0,
isValid: false,
};
}
}