lycwed-spritesheetjs
Version:
Spritesheet generator
394 lines (359 loc) • 12.1 kB
JavaScript
var exec = require("platform-command").exec;
var fs = require("fs");
var Mustache = require("mustache");
var async = require("async");
var os = require("os");
var path = require("path");
var crypto = require("crypto");
var tinify = require("tinify");
var ora = require("ora");
var spinnerSettings = {
build: "Building image processing...",
tinify: "TinyPNG processing...",
spinner: {
interval: 80,
frames: [
"[ ]",
"[> ]",
"[=> ]",
"[==> ]",
"[===> ]",
"[====> ]",
"[=====> ]",
"[======> ]",
"[=======> ]",
"[========> ]",
"[=========>]",
"[ =========]",
"[ ========]",
"[ =======]",
"[ ======]",
"[ =====]",
"[ ====]",
"[ ===]",
"[ ==]",
"[ =]",
],
},
};
const TINIPNG_API_KEY = process.env.TINIPNG_API_KEY;
var packing = require("./packing/packing.js");
var sorter = require("./sorter/sorter.js");
/**
* Generate temporary trimmed image files
* @param {string[]} files
* @param {object} options
* @param {string} options.scale image resize
* @param {string} options.fuzz image fuzz
* @param {boolean} options.trim is trimming enabled
* @param callback
*/
exports.treatImages = function (files, options, callback) {
// if (!options.trim) return callback(null);
var uuid = crypto.randomBytes(16).toString("hex");
var i = 0;
async.eachSeries(
files,
function (file, next) {
file.originalPath = file.path;
i++;
file.path = path.join(
os.tmpdir(),
"spritesheet_js_" + uuid + "_" + new Date().getTime() + "_image_" + i + ".png",
);
var resize = options.scale ? " -resize " + options.scale : "";
var fuzz = options.fuzz ? " -fuzz " + options.fuzz : "";
//have to add 1px transparent border because imagemagick does trimming based on border pixel's color
var trim = options.trim ? " -bordercolor transparent -border 1 -trim " : "";
var command =
"convert" +
fuzz +
' -define png:exclude-chunks=date "' +
file.originalPath +
'"' +
trim +
"" +
resize +
' "' +
file.path +
'"';
exec(command, next);
},
callback,
);
};
/**
* Iterates through given files and gets its size
* @param {string[]} files
* @param {object} options
* @param {boolean} options.trim is trimming enabled
* @param {function} callback
*/
exports.getImagesSizes = function (files, options, callback) {
var filePaths = files.map(function (file) {
return '"' + file.path + '"';
});
exec("identify " + filePaths.join(" "), function (err, stdout) {
if (err) return callback(err);
var sizes = stdout.split("\n");
sizes = sizes.splice(0, sizes.length - 1);
sizes.forEach(function (item, i) {
var size = item.match(/ ([0-9]+)x([0-9]+) /);
files[i].width = parseInt(size[1], 10) + options.padding * 2;
files[i].height = parseInt(size[2], 10) + options.padding * 2;
var forceTrimmed = false;
if (options.divisibleByTwo) {
if (files[i].width & 1) {
files[i].width += 1;
forceTrimmed = true;
}
if (files[i].height & 1) {
files[i].height += 1;
forceTrimmed = true;
}
}
files[i].area = files[i].width * files[i].height;
files[i].trimmed = false;
if (options.trim) {
var rect = item.match(/ ([0-9]+)x([0-9]+)[\+\-]([0-9]+)[\+\-]([0-9]+) /);
files[i].trim = {};
files[i].trim.x = parseInt(rect[3], 10) - 1;
files[i].trim.y = parseInt(rect[4], 10) - 1;
files[i].trim.width = parseInt(rect[1], 10) - 2;
files[i].trim.height = parseInt(rect[2], 10) - 2;
files[i].trimmed =
forceTrimmed ||
(files[i].trim.width !== files[i].width - options.padding * 2 ||
files[i].trim.height !== files[i].height - options.padding * 2);
}
});
callback(null, files);
});
};
/**
* Determines texture size using selected algorithm
* @param {object[]} files
* @param {object} options
* @param {object} options.algorithm (growing-binpacking, binpacking, vertical, horizontal)
* @param {object} options.square canvas width and height should be equal
* @param {object} options.powerOfTwo canvas width and height should be power of two
* @param {function} callback
*/
exports.determineCanvasSize = function (files, options, callback) {
files.forEach(function (item) {
item.w = item.width;
item.h = item.height;
});
// sort files based on the choosen options.sort method
sorter.run(options.sort, files);
packing.pack(options.algorithm, files, options);
if (options.square) {
options.width = options.height = Math.max(options.width, options.height);
}
if (options.powerOfTwo) {
options.width = roundToPowerOfTwo(options.width);
options.height = roundToPowerOfTwo(options.height);
}
callback(null, options);
};
/**
* generates texture data file
* @param {object[]} files
* @param {object} options
* @param {string} options.path path to image file
* @param {function} callback
*/
exports.generateImage = function (files, options, callback) {
var spinner = ora({ text: spinnerSettings.build, spinner: spinnerSettings.spinner });
spinner.start();
var command = [
"convert -define png:exclude-chunks=date -quality 0% -size " + options.width + "x" + options.height + " xc:none",
];
files.forEach(function (file) {
command.push(
'"' + file.path + '" -geometry +' + (file.x + options.padding) + "+" + (file.y + options.padding) + " -composite",
);
});
var filePath = options.path + "/" + options.name + ".png";
command.push('"' + filePath + '"');
exec(command.join(" "), function (err) {
if (err) {
spinner.fail("Spritesheet build fails...");
return callback(err);
}
function getImageInfos(filePath) {
return new Promise(function (resolve) {
exec("magick identify " + filePath, function (err, stdout) {
if (err) {
infos = "unable to retrieve infos...";
}
else {
params = stdout.split(" ");
infos = [
// "format: " + params[1],
"format: " + options.format,
"size: " + params[2],
"weight: " + params[6],
].join(" / ");
}
resolve(infos);
});
});
}
return getImageInfos(filePath).then(function (infos) {
spinner.succeed("YEAH! Spritesheet successfully generated! " + infos);
files.forEach(function (file) {
if (file.originalPath && file.originalPath !== file.path) {
fs.unlinkSync(file.path.replace(/\\ /g, " "));
}
});
var tinipngApiKey = TINIPNG_API_KEY || options.tinify;
if (tinipngApiKey) {
spinner = ora({ text: spinnerSettings.tinify, spinner: spinnerSettings.spinner });
spinner.start();
// var filePathOptimized = filePath.replace('.png', '-optimized.png');
// tinify
// .fromFile(filePath)
// .toFile(filePathOptimized).then(function() {}
tinify.key = tinipngApiKey;
return tinify.fromFile(filePath).toFile(filePath).then(
function () {
return getImageInfos(filePath).then(function (infos) {
spinner.succeed("WOOOOW! Spritesheet successfully tinified! " + infos);
return callback(null);
});
},
function (err) {
message = err;
if (err instanceof tinify.AccountError) {
message = err.message;
}
else if (err instanceof tinify.ClientError) {
// Check your source image and request options.
message = "tinify: generated image seems to be the problem";
}
else if (err instanceof tinify.ServerError) {
// Temporary issue with the Tinify API.
message = "tinify: issue with the API";
}
else if (err instanceof tinify.ConnectionError) {
// A network connection error occurred.
message = "tinify: network connection error";
}
spinner.fail("Sorry tinification fails...");
return callback(message);
},
);
}
else {
return callback(null);
}
});
});
};
/**
* generates texture data file
* @param {object[]} files
* @param {object} options
* @param {string} options.path path to data file
* @param {string} options.dataFile data file name
* @param {function} callback
*/
exports.generateData = function (files, options, callback) {
var formats = (Array.isArray(options.customFormat)
? options.customFormat
: [
options.customFormat,
]).concat(
Array.isArray(options.format)
? options.format
: [
options.format,
],
);
formats.forEach(function (format, i) {
if (!format) return;
var path = typeof format === "string" ? format : __dirname + "/../templates/" + format.template;
var templateContent = fs.readFileSync(path, "utf-8");
var cssPriority = 0;
var cssPriorityNormal = cssPriority++;
var cssPriorityHover = cssPriority++;
var cssPriorityActive = cssPriority++;
// sort files based on the choosen options.sort method
sorter.run(options.sort, files);
options.files = files;
options.files.forEach(function (item, i) {
item.spritesheetWidth = options.width;
item.spritesheetHeight = options.height;
item.width -= options.padding * 2;
item.height -= options.padding * 2;
item.x += options.padding;
item.y += options.padding;
item.index = i;
if (item.trim) {
item.trim.frameX = -item.trim.x;
item.trim.frameY = -item.trim.y;
item.trim.offsetX = Math.floor(Math.abs(item.trim.x + item.width / 2 - item.trim.width / 2));
item.trim.offsetY = Math.floor(Math.abs(item.trim.y + item.height / 2 - item.trim.height / 2));
}
item.cssName = item.name || "";
if (item.cssName.indexOf("_hover") >= 0) {
item.cssName = item.cssName.replace("_hover", ":hover");
item.cssPriority = cssPriorityHover;
}
else if (item.cssName.indexOf("_active") >= 0) {
item.cssName = item.cssName.replace("_active", ":active");
item.cssPriority = cssPriorityActive;
}
else {
item.cssPriority = cssPriorityNormal;
}
});
function getIndexOfCssName(files, cssName) {
for (var i = 0; i < files.length; ++i) {
if (files[i].cssName === cssName) {
return i;
}
}
return -1;
}
if (options.cssOrder) {
var order = options.cssOrder.replace(/\./g, "").split(",");
order.forEach(function (cssName) {
var index = getIndexOfCssName(files, cssName);
if (index >= 0) {
files[index].cssPriority = cssPriority++;
}
else {
console.warn("could not find :" + cssName + "css name");
}
});
}
options.files.sort(function (a, b) {
return a.cssPriority - b.cssPriority;
});
options.files[options.files.length - 1].isLast = true;
var result = Mustache.render(templateContent, options);
function findPriority(property) {
var value = options[property];
var isArray = Array.isArray(value);
if (isArray) {
return i < value.length ? value[i] : format[property] || value[0];
}
return format[property] || value;
}
fs.writeFile(findPriority("path") + "/" + findPriority("name") + "." + findPriority("extension"), result, callback);
});
};
/**
* Rounds a given number to to next number which is power of two
* @param {number} value number to be rounded
* @return {number} rounded number
*/
function roundToPowerOfTwo(value) {
var powers = 2;
while (value > powers) {
powers *= 2;
}
return powers;
}