Files
pixel.horse/src/ts/tools/palette-utils.ts
T
2019-08-28 21:15:02 -07:00

129 lines
3.7 KiB
TypeScript

import { Rect, ExtCanvas } from './types';
import { cropCanvas, createExtCanvas } from './canvas-utils';
const validPatternColors = [
255, // red
255 << 8, // green
255 | (255 << 8), // yellow
255 << 16, // blue
255 | (255 << 16), // purple
(255 << 8) | (255 << 16), // teal
255 | (255 << 8) | (255 << 16), // white
0, // black
];
const getAlpha = (alpha: number) => alpha < 15 ? false : (alpha > 250 ? true : null);
const isOutline = (shade: number) => shade <= 159;
const round255 = (n: number) => n > 200 ? 255 : (n < 50 ? 0 : n);
const rgbToColor = (r: number, g: number, b: number) => round255(r) | (round255(g) << 8) | (round255(b) << 16);
const findValidPatternColor = (r: number, g: number, b: number) => validPatternColors.indexOf(rgbToColor(r, g, b));
function getShade(shade: number) {
// outlines
// 135 - dark outline
// 159 - outline
// fills
// 174 - dark shades
// 204 - shade
// 217 - dark fill
// 255 - fill
if (shade > 130 && shade < 140) { // dark outline
return 135;
} else if (shade > 150 && shade < 165) { // outline
return 159;
} else if (shade > 165 && shade < 180) { // dark shade
return 174;
} else if (shade > 190 && shade < 210) { // shade
return 204;
} else if (shade > 210 && shade < 220) { // dark fill
return 217;
} else if (shade > 245) { // fill
return 255;
} else {
return -1;
}
}
function getShadeForShading(shade: number) {
if (shade === 135) { // dark outline
return 204;
} else if (shade === 159) { // outline
return 255;
} else {
return shade;
}
}
function pixel(data: Uint8ClampedArray, index: number) {
return data.slice(index, index + 4).join(', ');
}
export interface ColorsOutput {
colors?: number;
forceWhite?: boolean;
}
export function imageToPalette(
rect: Rect, image: ExtCanvas, pattern: ExtCanvas, palette: number[] | undefined, config: ColorsOutput
) {
const pat = cropCanvas(pattern, 0, 0, image.width, image.height);
const ctx = pat.getContext('2d')!;
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(image, 0, 0);
const imageData = image.getContext('2d')!.getImageData(rect.x, rect.y, rect.w, rect.h);
const pattData = ctx.getImageData(rect.x, rect.y, rect.w, rect.h);
const length = imageData.width * imageData.height * 4;
const data = imageData.data;
const pdata = pattData.data;
let maxIndex = 0;
for (let i = 0; i < length; i += 4) {
const x = ((i / 4) % rect.w) + rect.x;
const y = Math.floor((i / 4) / rect.h) + rect.y;
if (palette) {
const color = ((data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3]) >>> 0;
const index = palette.indexOf(color);
if (index === -1) {
throw new Error(`Invalid color (palette) [${pixel(data, i)}] [0x${color.toString(16)}] (${x} ${y}) (${image.info})`);
}
data[i] = index;
data[i + 1] = 255;
} else {
const alpha = getAlpha(data[i + 3]);
const shade = getShade(data[i]);
const index = findValidPatternColor(pdata[i], pdata[i + 1], pdata[i + 2]);
if (index === -1)
throw new Error(`Invalid color (pattern) [${pixel(pdata, i)}] (${x} ${y}) (${pat.info})`);
if (alpha == null)
throw new Error(`Invalid color [${pixel(data, i)}] (${x} ${y}) (${image.info})`);
const actualIndex = (index * 2) + (isOutline(shade) ? 2 : 1);
const shadeForShading = getShadeForShading(shade);
data[i] = alpha ? actualIndex : 0;
data[i + 1] = (config.forceWhite && shade !== -1) ? 255 : (alpha ? shadeForShading : 255);
if (alpha) {
maxIndex = Math.max(maxIndex, index);
}
}
data[i + 2] = 0;
data[i + 3] = 255;
}
config.colors = palette ? palette.length : ((maxIndex + 1) * 2 + 1);
const result = createExtCanvas(image.width, image.height, `${image.info} (image to palette)`);
result.getContext('2d')!.putImageData(imageData, rect.x, rect.y);
return result;
}