Skip to content

Commit

Permalink
Split CLI from main code (#3)
Browse files Browse the repository at this point in the history
* Flesh out the package.json a little

* Split out CLI from main code
  • Loading branch information
somewhatabstract committed Feb 14, 2018
1 parent 627dac4 commit 6ca55e1
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 119 deletions.
20 changes: 15 additions & 5 deletions README.md
Expand Up @@ -9,16 +9,26 @@ However, it might be jealous of all those parrots and their parties. Let's help

![Party Smiling Emoji](./party-smile.gif "Party Smiling Emoji")

## Usage
`node index.js smile.png party.gif`

Note: If you'd like to create emojis for Slack, make sure your input image is 128x128 (or less).

### Radius
## Usage
### CLI
`bin/ppp smile.png party.gif`

#### Radius
If you want to tune the radius of the animating circle, you can use the `--radius=<n>` option. The default for this is `10`.

For example, `node index.js --radius=0 smile.png party.gif` will create a party version of `smile.png` where the image has an ounce or two less party.
For example, `bin/ppp --radius=0 smile.png party.gif` will create a party version of `smile.png` where the image has an ounce or two less party.

![Still Party Smile Emoji](./still-party-smile.gif "Still Party Smile Emoji")

### Node
```
const fs = require("fs");
const PartyPartyParty = require("party-party-party");
const outputFileStream = fs.createWriteStream("my-output-file.gif");
PartyPartyParty("my-input.png", outputFileStream, 10);
```

![Party Heart Emoji](./heart.gif "Party Heart Emoji")
3 changes: 3 additions & 0 deletions bin/ppp
@@ -0,0 +1,3 @@
#!/usr/bin/env node

require('../lib/cli').run(__filename);
183 changes: 72 additions & 111 deletions index.js
@@ -1,33 +1,6 @@
const fs = require("fs");
const minimist = require("minimist");
const getPixels = require("get-pixels");
const gifEncoder = require("gif-encoder");

// Process the arguments we're launched with and get rid of anything we won't be using.
args = minimist(process.argv, {
default: {
"radius": 10, // 0 = regular stationary party
},
unknown: arg => {
// Filter out node and our index.js.
// This ensures that the args._ array will just be the files we process
if (arg === process.execPath) return false;
if (arg === __filename) return false;
return true;
}
});

//TODO(somewhatabstract): Make this code an importable module and then provide a simple wrapper for direct CLI usage
//TODO(somewhatabstract): Consider adding some scaling or a --fitslack option?
//TODO(somewhatabstract): Add ability to specify lots of different files at once.
//TODO(somewhatabstract): Add error checks and useful help text.
if (args._.length !== 2) {
console.log("Usage: " + __filename + " input.png output.gif");
process.exit(-1);
}

const inputFilename = args._[0];
const outputFilename = args._[1];
const toGreyscale = require("./lib/grayscale");

// The party palette. Party on, Sirocco!
const colours = [
Expand All @@ -43,100 +16,88 @@ const colours = [
[255, 105, 104]
];

//TODO(somewhatabstract): Add other variations to radius, like tilt (for bobbling side to side)
const partyOffset = [];
const partyRadius = parseInt(args.radius);
colours.forEach((c, colourIndex) => {
const x =
partyRadius * Math.sin(2 * Math.PI * (-colourIndex / colours.length));
const y =
partyRadius * Math.cos(2 * Math.PI * (-colourIndex / colours.length));
partyOffset.push([Math.round(x), Math.round(y)]);
});

function toGreyscale(pixels) {
const greyscale = [];

for (var i = 0; i < pixels.data.length / 4; i += 1) {
const idx = i * 4;
if (pixels.data[idx + 3] < 64) {
greyscale.push(-1);
} else {
const avg =
(pixels.data[idx] +
pixels.data[idx + 1] +
pixels.data[idx + 2]) /
3;
greyscale.push(avg);
}
}
return greyscale;
}

function imageData(err, pixels) {
if (err) {
console.log("Invalid image path..");
console.log(err);
return;
}

const { shape } = pixels;
const greyscale = toGreyscale(pixels);

const gif = new gifEncoder(shape[0], shape[1]);
const outputFile = fs.createWriteStream(outputFilename);
gif.pipe(outputFile);

gif.setDelay(50);
gif.setRepeat(0);
gif.setTransparent("0x00FF00");
gif.writeHeader();
gif.on("readable", function() {
gif.read();
/**
* Writes a party version of the given input image to the specified output stream.
* @param {string} inputFilename A GIF image file to be partified
* @param {stream.Writable} outputStream The stream where the partified image is to be written
* @param {number} partyRadius The radius used to animate movement in the output image
*/
function createPartyImage(inputFilename, outputStream, partyRadius) {
//TODO(somewhatabstract): Add other variations to radius, like tilt (for bobbling side to side)
const partyOffset = [];
colours.forEach((c, colourIndex) => {
const x =
partyRadius * Math.sin(2 * Math.PI * (-colourIndex / colours.length));
const y =
partyRadius * Math.cos(2 * Math.PI * (-colourIndex / colours.length));
partyOffset.push([Math.round(x), Math.round(y)]);
});

function getPixelValue(arr, shape, x, y) {
if (x < 0 || x >= shape[0] || y < 0 || y >= shape[1]) {
return -1;
function processImage(err, pixels) {
if (err) {
console.log("Invalid image path..");
console.log(err);
return;
}

return (result = arr[x + y * shape[0]]);
}
const { shape } = pixels;
const greyscale = toGreyscale(pixels);

colours.forEach(function(c, colourIndex) {
const offset = partyOffset[colourIndex];
const p = [];
const gif = new gifEncoder(shape[0], shape[1]);
gif.pipe(outputStream);

for (var y = 0; y < shape[1]; y += 1) {
for (var x = 0; x < shape[0]; x += 1) {
let g = getPixelValue(
greyscale,
shape,
x + offset[0],
y + offset[1]
);
gif.setDelay(50);
gif.setRepeat(0);
gif.setTransparent("0x00FF00");
gif.writeHeader();
gif.on("readable", function() {
gif.read();
});

if (g === -1) {
p.push(0);
p.push(255);
p.push(0);
p.push(0);
} else {
g = g < 32 ? 32 : g;
function getPixelValue(arr, shape, x, y) {
if (x < 0 || x >= shape[0] || y < 0 || y >= shape[1]) {
return -1;
}
return arr[x + y * shape[0]];
}

p.push(g * c[0] / 255);
p.push(g * c[1] / 255);
p.push(g * c[2] / 255);
p.push(255);
colours.forEach(function(c, colourIndex) {
const offset = partyOffset[colourIndex];
const p = [];

for (let y = 0; y < shape[1]; y += 1) {
for (let x = 0; x < shape[0]; x += 1) {
let g = getPixelValue(
greyscale,
shape,
x + offset[0],
y + offset[1]
);

if (g === -1) {
p.push(0);
p.push(255);
p.push(0);
p.push(0);
} else {
g = g < 32 ? 32 : g;

p.push(g * c[0] / 255);
p.push(g * c[1] / 255);
p.push(g * c[2] / 255);
p.push(255);
}
}
}
}

gif.addFrame(p);
gif.flushData();
});
gif.addFrame(p);
gif.flushData();
});

gif.finish();
}

gif.finish();
getPixels(inputFilename, processImage);
}

const input = getPixels(inputFilename, imageData);
module.exports = createPartyImage;
37 changes: 37 additions & 0 deletions lib/cli.js
@@ -0,0 +1,37 @@
const fs = require("fs");
const minimist = require("minimist");
const PartyPartyParty = require("../index");

function run(launchFilePath) {
// Process the arguments we're launched with and get rid of anything we won't be using.
args = minimist(process.argv, {
default: {
"radius": 10, // 0 = regular stationary party
},
unknown: arg => {
// Filter out node and our index.js.
// This ensures that the args._ array will just be the files we process
if (arg === process.execPath) return false;
if (arg === (launchFilePath || __filename)) return false;
return true;
}
});

//TODO(somewhatabstract): Consider adding some scaling or a --fitslack option?
//TODO(somewhatabstract): Add ability to specify lots of different files at once.
//TODO(somewhatabstract): Add error checks and useful help text.
if (args._.length !== 2) {
console.log("Usage: " + (launchFilePath || __filename) + " input.png output.gif");
process.exit(-1);
}

const inputFilename = args._[0];
const outputFilename = args._[1];

const outputFileStream = fs.createWriteStream(outputFilename);
PartyPartyParty(inputFilename, outputFileStream, parseInt(args.radius));
}

module.exports = {
run
}
20 changes: 20 additions & 0 deletions lib/grayscale.js
@@ -0,0 +1,20 @@
function toGreyscale(pixels) {
const greyscale = [];

for (var i = 0; i < pixels.data.length / 4; i += 1) {
const idx = i * 4;
if (pixels.data[idx + 3] < 64) {
greyscale.push(-1);
} else {
const avg =
(pixels.data[idx] +
pixels.data[idx + 1] +
pixels.data[idx + 2]) /
3;
greyscale.push(avg);
}
}
return greyscale;
}

module.exports = toGreyscale;
26 changes: 23 additions & 3 deletions package.json
Expand Up @@ -2,12 +2,32 @@
"name": "party-party-party",
"version": "1.0.0",
"description": "Turn a source image into an animated party emoji",
"main": "index.js",
"scripts": {
"test": "test"
"repository": {
"type": "git",
"url": "git://github.com/scotchfield/party-party-party.git"
},
"bugs": {
"url": "https://github.com/scotchfield/party-party-party/issues"
},
"author": "scotchfield",
"license": "MIT",
"contributors": [
"Scott Grant <swrittenb@gmail.com>",
"Jeff Yates <jeff@somewhatabstract.com>"
],
"keywords": [
"party",
"emoji",
"emoticon",
"images",
"animation"
],
"main": "index.js",
"bin": {
"ppp": "./bin/ppp"
},
"scripts": {
},
"dependencies": {
"get-pixels": "^3.3.0",
"gif-encoder": "^0.6.1",
Expand Down

0 comments on commit 6ca55e1

Please sign in to comment.