Files
perler-beads/src/utils/pixelation.ts

221 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { transparentColorData } from './pixelEditingUtils';
// 定义像素化模式
export enum PixelationMode {
Dominant = 'dominant', // 卡通模式(主色)
Average = 'average', // 真实模式(平均色)
}
// 定义色号系统类型
export type ColorSystem = 'MARD' | 'COCO' | '漫漫' | '盼盼' | '咪小窝';
// --- 必要的类型定义 ---
export interface RgbColor {
r: number;
g: number;
b: number;
}
export interface PaletteColor {
key: string;
hex: string;
rgb: RgbColor;
}
export interface MappedPixel {
key: string;
color: string;
isExternal?: boolean;
}
// --- 辅助函数 ---
// 转换 Hex 到 RGB
export function hexToRgb(hex: string): RgbColor | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// 计算颜色距离
export function colorDistance(rgb1: RgbColor, rgb2: RgbColor): number {
const dr = rgb1.r - rgb2.r;
const dg = rgb1.g - rgb2.g;
const db = rgb1.b - rgb2.b;
return Math.sqrt(dr * dr + dg * dg + db * db);
}
// 查找最接近的颜色
export function findClosestPaletteColor(
targetRgb: RgbColor,
palette: PaletteColor[]
): PaletteColor {
if (!palette || palette.length === 0) {
console.error("findClosestPaletteColor: Palette is empty or invalid!");
// 提供一个健壮的回退
return { key: 'ERR', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
}
let minDistance = Infinity;
let closestColor = palette[0];
for (const paletteColor of palette) {
const distance = colorDistance(targetRgb, paletteColor.rgb);
if (distance < minDistance) {
minDistance = distance;
closestColor = paletteColor;
}
if (distance === 0) break; // 完全匹配,提前退出
}
return closestColor;
}
// --- 核心像素化计算逻辑 ---
/**
* 计算图像指定区域的代表色(根据所选模式)
* @param imageData 包含像素数据的 ImageData 对象
* @param startX 区域起始 X 坐标
* @param startY 区域起始 Y 坐标
* @param width 区域宽度
* @param height 区域高度
* @param mode 计算模式 ('dominant' 或 'average')
* @returns 代表色的 RGB 对象,或 null如果区域无效或全透明
*/
function calculateCellRepresentativeColor(
imageData: ImageData,
startX: number,
startY: number,
width: number,
height: number,
mode: PixelationMode
): RgbColor | null {
const data = imageData.data;
const imgWidth = imageData.width;
let rSum = 0, gSum = 0, bSum = 0;
let pixelCount = 0;
const colorCountsInCell: { [key: string]: number } = {};
let dominantColorRgb: RgbColor | null = null;
let maxCount = 0;
const endX = startX + width;
const endY = startY + height;
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const index = (y * imgWidth + x) * 4;
// 检查 alpha 通道,忽略完全透明的像素
if (data[index + 3] < 128) continue;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
pixelCount++;
if (mode === PixelationMode.Average) {
rSum += r;
gSum += g;
bSum += b;
} else { // Dominant mode
const colorKey = `${r},${g},${b}`;
colorCountsInCell[colorKey] = (colorCountsInCell[colorKey] || 0) + 1;
if (colorCountsInCell[colorKey] > maxCount) {
maxCount = colorCountsInCell[colorKey];
dominantColorRgb = { r, g, b };
}
}
}
}
if (pixelCount === 0) {
return null; // 区域内没有不透明像素
}
if (mode === PixelationMode.Average) {
return {
r: Math.round(rSum / pixelCount),
g: Math.round(gSum / pixelCount),
b: Math.round(bSum / pixelCount),
};
} else { // Dominant mode
return dominantColorRgb; // 可能为 null 如果只有一个透明像素
}
}
/**
* 根据原始图像数据、网格尺寸、调色板和模式计算像素化网格数据。
* @param originalCtx 原始图像的 Canvas 2D Context
* @param imgWidth 原始图像宽度
* @param imgHeight 原始图像高度
* @param N 网格横向数量
* @param M 网格纵向数量
* @param palette 当前使用的调色板
* @param mode 像素化模式 (Dominant/Average)
* @param t1FallbackColor T1 或其他备用颜色数据
* @returns 计算后的 MappedPixel 网格数据
*/
export function calculatePixelGrid(
originalCtx: CanvasRenderingContext2D,
imgWidth: number,
imgHeight: number,
N: number,
M: number,
palette: PaletteColor[],
mode: PixelationMode,
t1FallbackColor: PaletteColor // 传入备用色
): MappedPixel[][] {
console.log(`Calculating pixel grid with mode: ${mode}`);
const mappedData: MappedPixel[][] = Array(M).fill(null).map(() => Array(N).fill({ key: t1FallbackColor.key, color: t1FallbackColor.hex }));
const cellWidthOriginal = imgWidth / N;
const cellHeightOriginal = imgHeight / M;
let fullImageData: ImageData | null = null;
try {
fullImageData = originalCtx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
console.error("Failed to get full image data:", e);
// 如果无法获取图像数据,返回一个空的或默认的网格
return mappedData;
}
for (let j = 0; j < M; j++) {
for (let i = 0; i < N; i++) {
const startXOriginal = Math.floor(i * cellWidthOriginal);
const startYOriginal = Math.floor(j * cellHeightOriginal);
// 计算精确的单元格结束位置,避免超出图像边界
const endXOriginal = Math.min(imgWidth, Math.ceil((i + 1) * cellWidthOriginal));
const endYOriginal = Math.min(imgHeight, Math.ceil((j + 1) * cellHeightOriginal));
// 计算实际的单元格宽高
const currentCellWidth = Math.max(1, endXOriginal - startXOriginal);
const currentCellHeight = Math.max(1, endYOriginal - startYOriginal);
// 使用提取的函数计算代表色
const representativeRgb = calculateCellRepresentativeColor(
fullImageData,
startXOriginal,
startYOriginal,
currentCellWidth,
currentCellHeight,
mode
);
let finalCellColorData: MappedPixel;
if (representativeRgb) {
const closestBead = findClosestPaletteColor(representativeRgb, palette);
finalCellColorData = { key: closestBead.key, color: closestBead.hex };
} else {
// 如果单元格为空或全透明,标记为透明/外部
finalCellColorData = { ...transparentColorData };
}
mappedData[j][i] = finalCellColorData;
}
}
console.log(`Pixel grid calculation complete for mode: ${mode}`);
return mappedData;
}