|
|
@ -1,7 +1,7 @@ |
|
|
|
/** |
|
|
|
* Render |
|
|
|
* Render a recording file as an animated gif image |
|
|
|
* |
|
|
|
* |
|
|
|
* @author Mohammad Fares <faressoft.com@gmail.com> |
|
|
|
*/ |
|
|
|
|
|
|
@ -13,37 +13,40 @@ |
|
|
|
* @return {ProgressBar} |
|
|
|
*/ |
|
|
|
function getProgressBar(operation, framesCount) { |
|
|
|
|
|
|
|
return new di.ProgressBar(operation + ' ' + di.chalk.magenta('frame :current/:total') + ' :percent [:bar] :etas', { |
|
|
|
width: 30, |
|
|
|
total: framesCount |
|
|
|
}); |
|
|
|
|
|
|
|
return new di.ProgressBar( |
|
|
|
operation + |
|
|
|
" " + |
|
|
|
di.chalk.magenta("frame :current/:total") + |
|
|
|
" :percent [:bar] :etas", |
|
|
|
{ |
|
|
|
width: 30, |
|
|
|
total: framesCount, |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Write the recording data into render/data.json |
|
|
|
* |
|
|
|
* |
|
|
|
* @param {Object} recordingFile |
|
|
|
* @return {Promise} |
|
|
|
*/ |
|
|
|
function writeRecordingData(recordingFile) { |
|
|
|
|
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
// Write the data into data.json file in the root path of the app
|
|
|
|
di.fs.writeFile(di.path.join(ROOT_PATH, 'render/data.json'), JSON.stringify(recordingFile.json), 'utf8', function(error) { |
|
|
|
di.fs.writeFile( |
|
|
|
di.path.join(ROOT_PATH, "render/data.json"), |
|
|
|
JSON.stringify(recordingFile.json), |
|
|
|
"utf8", |
|
|
|
function (error) { |
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
} |
|
|
|
|
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
resolve(); |
|
|
|
} |
|
|
|
|
|
|
|
resolve(); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
); |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@ -53,80 +56,72 @@ function writeRecordingData(recordingFile) { |
|
|
|
* @return {Promise} resolve with the parsed PNG image |
|
|
|
*/ |
|
|
|
function loadPNG(path) { |
|
|
|
|
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
|
|
|
|
di.fs.readFile(path, function(error, imageData) { |
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
di.fs.readFile(path, function (error, imageData) { |
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
} |
|
|
|
|
|
|
|
new di.PNG().parse(imageData, function(error, data) { |
|
|
|
|
|
|
|
new di.PNG().parse(imageData, function (error, data) { |
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
} |
|
|
|
|
|
|
|
resolve(data); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Get the dimensions of the first rendered frame |
|
|
|
* |
|
|
|
* |
|
|
|
* @return {Promise} |
|
|
|
*/ |
|
|
|
function getFrameDimensions() { |
|
|
|
|
|
|
|
// The path of the first rendered frame
|
|
|
|
var framePath = di.path.join(ROOT_PATH, 'render/frames/0.png'); |
|
|
|
var framePath = di.path.join(ROOT_PATH, "render/frames/0.png"); |
|
|
|
|
|
|
|
// Read and parse a PNG image file
|
|
|
|
return loadPNG(framePath).then(function(png) { |
|
|
|
|
|
|
|
return({ |
|
|
|
return loadPNG(framePath).then(function (png) { |
|
|
|
return { |
|
|
|
width: png.width, |
|
|
|
height: png.height |
|
|
|
}); |
|
|
|
|
|
|
|
height: png.height, |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Render the frames into PNG images |
|
|
|
* |
|
|
|
* |
|
|
|
* @param {Array} records [{delay, content}, ...] |
|
|
|
* @param {Object} options {step} |
|
|
|
* @return {Promise} |
|
|
|
*/ |
|
|
|
function renderFrames(records, options) { |
|
|
|
|
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
// The number of frames
|
|
|
|
var framesCount = records.length; |
|
|
|
|
|
|
|
// Create a progress bar
|
|
|
|
var progressBar = getProgressBar('Rendering', Math.ceil(framesCount / options.step)); |
|
|
|
var progressBar = getProgressBar( |
|
|
|
"Rendering", |
|
|
|
Math.ceil(framesCount / options.step) |
|
|
|
); |
|
|
|
|
|
|
|
// Execute the rendering process
|
|
|
|
var render = di.spawn(di.electron, [di.path.join(ROOT_PATH, 'render/index.js'), options.step], {detached: false}); |
|
|
|
var render = di.spawn( |
|
|
|
di.electron, |
|
|
|
[di.path.join(ROOT_PATH, "render/index.js"), options.step], |
|
|
|
{ detached: false } |
|
|
|
); |
|
|
|
|
|
|
|
render.stderr.on('data', function(error) { |
|
|
|
render.stderr.on("data", function (error) { |
|
|
|
render.kill(); |
|
|
|
reject(new Error(error)); |
|
|
|
}); |
|
|
|
|
|
|
|
render.stdout.on('data', function(data) { |
|
|
|
|
|
|
|
render.stdout.on("data", function (data) { |
|
|
|
// Is not a recordIndex (to skip Electron's logs or new lines)
|
|
|
|
if (di.is.not.number(parseInt(data.toString()))) { |
|
|
|
return; |
|
|
@ -138,25 +133,20 @@ function renderFrames(records, options) { |
|
|
|
if (progressBar.complete) { |
|
|
|
resolve(); |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Merge the rendered frames into an animated GIF image |
|
|
|
* |
|
|
|
* |
|
|
|
* @param {Array} records [{delay, content}, ...] |
|
|
|
* @param {Object} options {quality, repeat, step, outputFile} |
|
|
|
* @param {Object} frameDimensions {width, height} |
|
|
|
* @return {Promise} |
|
|
|
*/ |
|
|
|
function mergeFrames(records, options, frameDimensions) { |
|
|
|
|
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
// The number of frames
|
|
|
|
var framesCount = records.length; |
|
|
|
|
|
|
@ -164,11 +154,14 @@ function mergeFrames(records, options, frameDimensions) { |
|
|
|
var stepsCounter = 0; |
|
|
|
|
|
|
|
// Create a progress bar
|
|
|
|
var progressBar = getProgressBar('Merging', Math.ceil(framesCount / options.step)); |
|
|
|
var progressBar = getProgressBar( |
|
|
|
"Merging", |
|
|
|
Math.ceil(framesCount / options.step) |
|
|
|
); |
|
|
|
|
|
|
|
// The gif image
|
|
|
|
var gif = new di.GIFEncoder(frameDimensions.width, frameDimensions.height, { |
|
|
|
highWaterMark: 5 * 1024 * 1024 |
|
|
|
highWaterMark: 5 * 1024 * 1024, |
|
|
|
}); |
|
|
|
|
|
|
|
// Pipe
|
|
|
@ -184,101 +177,91 @@ function mergeFrames(records, options, frameDimensions) { |
|
|
|
gif.writeHeader(); |
|
|
|
|
|
|
|
// Foreach frame
|
|
|
|
di.async.eachOfSeries(records, function(frame, index, callback) { |
|
|
|
di.async.eachOfSeries( |
|
|
|
records, |
|
|
|
function (frame, index, callback) { |
|
|
|
if (stepsCounter != 0) { |
|
|
|
stepsCounter = (stepsCounter + 1) % options.step; |
|
|
|
return callback(); |
|
|
|
} |
|
|
|
|
|
|
|
if (stepsCounter != 0) { |
|
|
|
stepsCounter = (stepsCounter + 1) % options.step; |
|
|
|
return callback(); |
|
|
|
} |
|
|
|
|
|
|
|
stepsCounter = (stepsCounter + 1) % options.step; |
|
|
|
|
|
|
|
// The path of the rendered frame
|
|
|
|
var framePath = di.path.join(ROOT_PATH, 'render/frames', index + '.png'); |
|
|
|
|
|
|
|
// Read and parse the rendered frame
|
|
|
|
loadPNG(framePath).then(function(png) { |
|
|
|
|
|
|
|
progressBar.tick(); |
|
|
|
|
|
|
|
// Set the duration (the delay of the next frame)
|
|
|
|
// The % is used to take the delay of the first frame
|
|
|
|
// as the duration of the last frame
|
|
|
|
gif.setDelay(records[(index + 1) % framesCount].delay); |
|
|
|
|
|
|
|
// Add frames
|
|
|
|
gif.addFrame(png.data); |
|
|
|
|
|
|
|
// Next
|
|
|
|
callback(); |
|
|
|
|
|
|
|
}).catch(function(error) { |
|
|
|
|
|
|
|
callback(error); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}, function(error) { |
|
|
|
// The path of the rendered frame
|
|
|
|
var framePath = di.path.join( |
|
|
|
ROOT_PATH, |
|
|
|
"render/frames", |
|
|
|
index + ".png" |
|
|
|
); |
|
|
|
|
|
|
|
// Read and parse the rendered frame
|
|
|
|
loadPNG(framePath) |
|
|
|
.then(function (png) { |
|
|
|
progressBar.tick(); |
|
|
|
|
|
|
|
// Set the duration (the delay of the next frame)
|
|
|
|
// The % is used to take the delay of the first frame
|
|
|
|
// as the duration of the last frame
|
|
|
|
gif.setDelay(records[(index + 1) % framesCount].delay); |
|
|
|
|
|
|
|
// Add frames
|
|
|
|
gif.addFrame(png.data); |
|
|
|
|
|
|
|
// Next
|
|
|
|
callback(); |
|
|
|
}) |
|
|
|
.catch(function (error) { |
|
|
|
callback(error); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function (error) { |
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
} |
|
|
|
|
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
// Write the footer
|
|
|
|
gif.finish(); |
|
|
|
resolve(); |
|
|
|
} |
|
|
|
|
|
|
|
// Write the footer
|
|
|
|
gif.finish(); |
|
|
|
resolve(); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
); |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Delete the temporary rendered PNG images |
|
|
|
* |
|
|
|
* Delete the temporary rendered PNG images |
|
|
|
* |
|
|
|
* @return {Promise} |
|
|
|
*/ |
|
|
|
function cleanup() { |
|
|
|
|
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
|
|
|
|
di.fs.emptyDir(di.path.join(ROOT_PATH, 'render/frames'), function(error) { |
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
di.fs.emptyDir(di.path.join(ROOT_PATH, "render/frames"), function (error) { |
|
|
|
if (error) { |
|
|
|
return reject(error); |
|
|
|
} |
|
|
|
|
|
|
|
resolve(); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Executed after the command completes its task |
|
|
|
* |
|
|
|
* |
|
|
|
* @param {String} outputFile the path of the rendered image |
|
|
|
*/ |
|
|
|
function done(outputFile) { |
|
|
|
|
|
|
|
console.log('\n' + di.chalk.green('Successfully Rendered')); |
|
|
|
console.log('The animated GIF image is saved into the file:'); |
|
|
|
console.log("\n" + di.chalk.green("Successfully Rendered")); |
|
|
|
console.log("The animated GIF image is saved into the file:"); |
|
|
|
console.log(di.chalk.magenta(outputFile)); |
|
|
|
process.exit(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* The command's main function |
|
|
|
* |
|
|
|
* |
|
|
|
* @param {Object} argv |
|
|
|
*/ |
|
|
|
function command(argv) { |
|
|
|
|
|
|
|
// Frames
|
|
|
|
var records = argv.recordingFile.json.records; |
|
|
|
var config = argv.recordingFile.json.config; |
|
|
@ -287,17 +270,20 @@ function command(argv) { |
|
|
|
var framesCount = records.length; |
|
|
|
|
|
|
|
// The path of the output file
|
|
|
|
var outputFile = di.utility.resolveFilePath('render' + (new Date()).getTime(), 'gif'); |
|
|
|
var outputFile = di.utility.resolveFilePath( |
|
|
|
"render" + new Date().getTime(), |
|
|
|
"gif" |
|
|
|
); |
|
|
|
|
|
|
|
// For adjusting (calculating) the frames delays
|
|
|
|
var adjustFramesDelaysOptions = { |
|
|
|
frameDelay: config.frameDelay, |
|
|
|
maxIdleTime: config.maxIdleTime |
|
|
|
maxIdleTime: config.maxIdleTime, |
|
|
|
}; |
|
|
|
|
|
|
|
// For rendering the frames into PNG images
|
|
|
|
var renderingOptions = { |
|
|
|
step: argv.step |
|
|
|
step: argv.step, |
|
|
|
}; |
|
|
|
|
|
|
|
// For merging the rendered frames into an animated GIF image
|
|
|
@ -305,7 +291,7 @@ function command(argv) { |
|
|
|
quality: config.quality, |
|
|
|
repeat: config.repeat, |
|
|
|
step: argv.step, |
|
|
|
outputFile: outputFile |
|
|
|
outputFile: outputFile, |
|
|
|
}; |
|
|
|
|
|
|
|
// Overwrite the quality of the rendered image
|
|
|
@ -320,35 +306,37 @@ function command(argv) { |
|
|
|
} |
|
|
|
|
|
|
|
// Tasks
|
|
|
|
di.asyncPromises.waterfall([ |
|
|
|
|
|
|
|
// Remove all previously rendered frames
|
|
|
|
cleanup, |
|
|
|
|
|
|
|
// Write the recording data into render/data.json
|
|
|
|
di._.partial(writeRecordingData, argv.recordingFile), |
|
|
|
|
|
|
|
// Render the frames into PNG images
|
|
|
|
di._.partial(renderFrames, records, renderingOptions), |
|
|
|
|
|
|
|
// Adjust frames delays
|
|
|
|
di._.partial(di.commands.play.adjustFramesDelays, records, adjustFramesDelaysOptions), |
|
|
|
|
|
|
|
// Get the dimensions of the first rendered frame
|
|
|
|
di._.partial(getFrameDimensions), |
|
|
|
|
|
|
|
// Merge the rendered frames into an animated GIF image
|
|
|
|
di._.partial(mergeFrames, records, mergingOptions), |
|
|
|
|
|
|
|
// Delete the temporary rendered PNG images
|
|
|
|
cleanup |
|
|
|
|
|
|
|
]).then(function() { |
|
|
|
|
|
|
|
done(outputFile); |
|
|
|
|
|
|
|
}).catch(di.errorHandler); |
|
|
|
|
|
|
|
di.asyncPromises |
|
|
|
.waterfall([ |
|
|
|
// Remove all previously rendered frames
|
|
|
|
cleanup, |
|
|
|
|
|
|
|
// Write the recording data into render/data.json
|
|
|
|
di._.partial(writeRecordingData, argv.recordingFile), |
|
|
|
|
|
|
|
// Render the frames into PNG images
|
|
|
|
di._.partial(renderFrames, records, renderingOptions), |
|
|
|
|
|
|
|
// Adjust frames delays
|
|
|
|
di._.partial( |
|
|
|
di.commands.play.adjustFramesDelays, |
|
|
|
records, |
|
|
|
adjustFramesDelaysOptions |
|
|
|
), |
|
|
|
|
|
|
|
// Get the dimensions of the first rendered frame
|
|
|
|
di._.partial(getFrameDimensions), |
|
|
|
|
|
|
|
// Merge the rendered frames into an animated GIF image
|
|
|
|
di._.partial(mergeFrames, records, mergingOptions), |
|
|
|
|
|
|
|
// Delete the temporary rendered PNG images
|
|
|
|
cleanup, |
|
|
|
]) |
|
|
|
.then(function () { |
|
|
|
done(outputFile); |
|
|
|
}) |
|
|
|
.catch(di.errorHandler); |
|
|
|
} |
|
|
|
|
|
|
|
////////////////////////////////////////////////////
|
|
|
@ -359,13 +347,13 @@ function command(argv) { |
|
|
|
* Command's usage |
|
|
|
* @type {String} |
|
|
|
*/ |
|
|
|
module.exports.command = 'render <recordingFile>'; |
|
|
|
module.exports.command = "render <recordingFile>"; |
|
|
|
|
|
|
|
/** |
|
|
|
* Command's description |
|
|
|
* @type {String} |
|
|
|
*/ |
|
|
|
module.exports.describe = 'Render a recording file as an animated gif image'; |
|
|
|
module.exports.describe = "Render a recording file as an animated gif image"; |
|
|
|
|
|
|
|
/** |
|
|
|
* Command's handler function |
|
|
@ -375,42 +363,40 @@ module.exports.handler = command; |
|
|
|
|
|
|
|
/** |
|
|
|
* Builder |
|
|
|
* |
|
|
|
* |
|
|
|
* @param {Object} yargs |
|
|
|
*/ |
|
|
|
module.exports.builder = function(yargs) { |
|
|
|
|
|
|
|
module.exports.builder = function (yargs) { |
|
|
|
// Define the recordingFile argument
|
|
|
|
yargs.positional('recordingFile', { |
|
|
|
describe: 'The recording file', |
|
|
|
type: 'string', |
|
|
|
coerce: di.utility.loadYAML |
|
|
|
yargs.positional("recordingFile", { |
|
|
|
describe: "The recording file", |
|
|
|
type: "string", |
|
|
|
coerce: di.utility.loadYAML, |
|
|
|
}); |
|
|
|
|
|
|
|
// Define the output option
|
|
|
|
yargs.option('o', { |
|
|
|
alias: 'output', |
|
|
|
type: 'string', |
|
|
|
describe: 'A name for the output file', |
|
|
|
yargs.option("o", { |
|
|
|
alias: "output", |
|
|
|
type: "string", |
|
|
|
describe: "A name for the output file", |
|
|
|
requiresArg: true, |
|
|
|
coerce: di._.partial(di.utility.resolveFilePath, di._, 'gif') |
|
|
|
coerce: di._.partial(di.utility.resolveFilePath, di._, "gif"), |
|
|
|
}); |
|
|
|
|
|
|
|
// Define the quality option
|
|
|
|
yargs.option('q', { |
|
|
|
alias: 'quality', |
|
|
|
type: 'number', |
|
|
|
describe: 'The quality of the rendered image (1 - 100)', |
|
|
|
requiresArg: true |
|
|
|
yargs.option("q", { |
|
|
|
alias: "quality", |
|
|
|
type: "number", |
|
|
|
describe: "The quality of the rendered image (1 - 100)", |
|
|
|
requiresArg: true, |
|
|
|
}); |
|
|
|
|
|
|
|
// Define the quality option
|
|
|
|
yargs.option('s', { |
|
|
|
alias: 'step', |
|
|
|
type: 'number', |
|
|
|
describe: 'To reduce the number of rendered frames (step > 1)', |
|
|
|
yargs.option("s", { |
|
|
|
alias: "step", |
|
|
|
type: "number", |
|
|
|
describe: "To reduce the number of rendered frames (step > 1)", |
|
|
|
requiresArg: true, |
|
|
|
default: 1 |
|
|
|
default: 1, |
|
|
|
}); |
|
|
|
|
|
|
|
}; |