///
import * as fs from 'fs';
import * as path from 'path';
import { spawn } from 'child_process';
import { reduce, flatten, map, dropRightWhile } from 'lodash';
import { Layer, Psd, ExtCanvas, Sprite, Rect, Result, ColorExtra } from './types';
import { ColorsOutput, imageToPalette } from './palette-utils';
import { getSpriteRect } from './sprite-sheet';
import { createColorCanvas, isCanvasEmpty } from './canvas-utils';
(global as any).DEVELOPMENT = true;
export const TRANSPARENT = 0;
export const BLACK = 0x000000ff;
export const WHITE = 0xffffffff;
export const OUTLINE_COLOR = 0x9f9f9fff;
export const SHADE_COLOR = 0xccccccff;
export const LIGHT_SHADE_COLOR = 0xddddddff;
export const TEAR_COLOR = 0xc1eef0ff;
export const DARK_GRAY = 0x232728ff;
export const MOUTH_COLOR = 0x721946ff;
export const TONGUE_COLOR = 0xf39f4bff;
export const TEETH_COLOR = 0x8cffffff;
export const TEETH_SHADE_COLOR = 0x77d9d9ff;
// TEMP: remove after adding palettes for effects
const holdPoofColors = [
0xffff47ff, 0xeaed58ff, 0xd8dc00ff, 0xff6741ff, 0xff0000ff, 0xff7af9ff, 0xff00ccff, 0xb67affff,
0x9876ffff, 0x76a6ffff, 0x0097ffff, 0x00ff9bff, 0x76ed5cff, 0x28dc00ff, 0x76ed5cff,
];
export const defaultPalette = [
TRANSPARENT, WHITE, BLACK, MOUTH_COLOR, TONGUE_COLOR, LIGHT_SHADE_COLOR, TEAR_COLOR, DARK_GRAY,
...holdPoofColors,
];
export function cartesian(a: A[], b: B[]): [A, B][];
export function cartesian(a: A[], b: B[], c: C[]): [A, B, C][];
export function cartesian(...args: any[]) {
return reduce(args, (a, b) => flatten(map(a, x => map(b as any, y => x.concat([y])))), [[]]);
}
export function mkdir(dirpath: string) {
try {
fs.mkdirSync(dirpath);
} catch { }
}
const isDirectory = (dir: string) => fs.lstatSync(dir).isDirectory();
export const getDirectories = (dir: string) => fs.readdirSync(dir).map(name => path.join(dir, name)).filter(isDirectory);
function findLayerByPath([name, ...child]: string[], layer: Layer | Psd | undefined): Layer | undefined {
return name ? findLayerByPath(child, layer && findByName(layer.children, name)) : layer;
}
export function findLayer(path: string, layer: Layer | Psd | undefined): Layer | undefined {
return findLayerByPath(path.split('/'), layer);
}
export function findLayerSafe(name: string, parent: Layer | Psd): Layer {
const layer = findLayer(name, parent);
if (!layer) {
throw new Error(`Missing layer "${name}" in "${parent.info}"`);
}
return layer;
}
export function findByName(items: T[], name: string): T | undefined {
return items.find(i => i.name === name);
}
export function findByIndex(items: T[], index: number) {
return items.find(i => i.index === index);
}
export const nameMatches = (regex: RegExp) => (l: { name: string; }) => regex.test(l.name);
export function compareNames(a: { name: string }, b: { name: string }) {
return a.name.localeCompare(b.name);
}
export const time = (function () {
const start = Date.now();
let last = start;
return function (text: string) {
console.log(text, (Date.now() - last), 'ms');
last = Date.now();
return true;
};
})();
export function spawnAsync(command: string, args?: string[]) {
return new Promise((resolve, reject) => {
spawn(command, args)
.on('error', (err: Error) => reject(err))
.on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Non-zero return code for ${command} (${code})`)));
});
}
// canvas
export function getCanvas(layer: Layer | undefined): ExtCanvas | undefined {
if (!layer)
return undefined;
const canvas = layer.canvas;
if (canvas) {
canvas.info = layer.info;
}
return canvas;
}
export function getCanvasSafe(layer: Layer): ExtCanvas {
const canvas = getCanvas(layer);
if (!canvas) {
throw new Error(`Cannot find canvas in layer "${layer.info}"`);
}
return canvas;
}
export function getLayerCanvas(name: string, parent: Layer | Psd | undefined) {
return getCanvas(findLayer(name, parent));
}
export function getLayerCanvasSafe(name: string, parent: Layer | Psd): ExtCanvas {
return getCanvasSafe(findLayerSafe(name, parent));
}
export function parseWithNumber(name: string) {
const match = /(\d+)/.exec(name);
return parseInt(match ? match[1] : '0', 10);
}
export const matcher = (regex: RegExp) => (text: string) => regex.test(text);
const isArrayEmpty = (a: T[] | null) => !a || a.length === 0;
const nullForEmpty = (a: T[] | null) => isArrayEmpty(a) ? null : a;
export function trimRight(items: ((T | null)[] | null)[]): ((T | null)[] | null)[] {
return dropRightWhile(items.map(nullForEmpty) as any, isArrayEmpty as any) as any;
}
// sprites
const redCanvas = createColorCanvas(1000, 1000, 'red');
export function addImage(images: HTMLCanvasElement[], canvas: HTMLCanvasElement) {
if (canvas) {
// TODO: remove duplicated
images.push(canvas);
return images.length - 1;
} else {
return 0;
}
}
export function createSprite(index: number, image: HTMLCanvasElement, { w, h, x, y }: Rect) {
return { index, image, w, h, x: 0, y: 0, ox: x, oy: y };
}
const maxSpriteWidth = 500;
const maxSpriteHeight = 500;
export function addSprite(
sprites: Sprite[], canvas?: ExtCanvas, pattern?: ExtCanvas, palette?: number[], out: ColorsOutput = {}
): number {
if (canvas) {
const rect = getSpriteRect(canvas, 0, 0, canvas.width, canvas.height);
if (rect.w && rect.h) {
if (rect.w > maxSpriteWidth || rect.h > maxSpriteHeight) {
throw new Error(`Sprite too large (${rect.w}, ${rect.h}) from [${canvas.info}]`);
}
const image = imageToPalette(rect, canvas, pattern || redCanvas, palette, out);
sprites.push(createSprite(sprites.length, image, rect));
return sprites.length - 1;
}
}
return 0;
}
export function addSpriteWithColors(
sprites: Sprite[], colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean
): ColorExtra {
const out: ColorsOutput = { forceWhite };
const color = addSprite(sprites, colorImage, patternImage, undefined, out);
return { color, colors: out.colors! };
}
export function getColorsCount(colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean): number {
const out: ColorsOutput = { forceWhite };
addSprite([], colorImage, patternImage, undefined, out);
return out.colors!;
}
export function createPixelSprites({ objects, objects2, images, sprites }: Result) {
const pixel = createColorCanvas(3, 3, 'white');
objects['pixelRect'] = addImage(images, pixel);
objects2['pixelRect2'] = addSprite(sprites, pixel, undefined, defaultPalette);
}
// layers
export const compareLayers = (a: Layer, b: Layer) => parseWithNumber(a.name) - parseWithNumber(b.name);
export function getPatternLayers(layer: Layer) {
return layer.children.filter(nameMatches(/^pattern/)).sort(compareLayers);
}
export function getPatternCanvases(layer: Layer): ExtCanvas[] {
const canvases = getPatternLayers(layer).map(getCanvas);
return dropRightWhile(canvases, isCanvasEmpty) as ExtCanvas[];
}
export function clipPattern(color: ExtCanvas, pattern: ExtCanvas | undefined): ExtCanvas | undefined {
if (pattern) {
const ctx = pattern.getContext('2d')!;
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(color, 0, 0);
}
return pattern;
}