├── rollup.config.js ├── test └── local-file-test.js ├── package.json ├── LICENSE ├── README.md ├── bin └── partyfy-cli.js ├── .gitignore └── src └── partyfy.js /rollup.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | module.exports = [ 4 | { 5 | input: 'src/partyfy.js', 6 | output: [ 7 | { file: pkg.main, format: 'cjs' }, 8 | { file: pkg.module, format: 'es' } 9 | ] 10 | } 11 | ]; 12 | -------------------------------------------------------------------------------- /test/local-file-test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const partyfy = require('../src/partyfy'); 5 | 6 | async function main() { 7 | const [, , inputFile, outputFile] = process.argv; 8 | try { 9 | const imageFile = fs.readFileSync(path.resolve(inputFile)); 10 | 11 | const partyImage = await partyfy(imageFile); 12 | 13 | fs.writeFileSync(path.resolve(outputFile), partyImage); 14 | } catch (err) { 15 | console.log(err); 16 | } 17 | } 18 | 19 | main(); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyfy", 3 | "version": "0.1.7", 4 | "description": "A Node.js library for generating rainbow party gifs", 5 | "main": "dist/partyfy.cjs.js", 6 | "module": "dist/partyfy.esm.js", 7 | "source": "src/partyfy.js", 8 | "author": "Dillon Mulroy", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/dmmulroy/partyfy.git" 13 | }, 14 | "scripts": { 15 | "build": "rollup -c", 16 | "dev": "rollup -c -w", 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "preinstall": "npx npm-force-resolutions" 19 | }, 20 | "bin": { 21 | "partyfy": "bin/partyfy-cli.js" 22 | }, 23 | "dependencies": { 24 | "commander": "^3.0.2", 25 | "file-type": "^12.2.0", 26 | "get-pixels": "^3.3.2", 27 | "gifwrap": "^0.9.2" 28 | }, 29 | "devDependencies": { 30 | "npm-force-resolutions": "^0.0.3", 31 | "rollup": "^1.19.4" 32 | }, 33 | "files": [ 34 | "dist" 35 | ], 36 | "resolutions": { 37 | "jpeg-js": "0.4.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dillon Mulroy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # partyfy 2 | 3 | [![npm version](https://badge.fury.io/js/partyfy.svg)](https://badge.fury.io/js/partyfy) 4 | 5 | ## Usage 6 | 7 | ### `partyfy(file[, options])` 8 | 9 | - file `` 10 | - options `` 11 | - frameDelay `` Speed in milliseconds between frames. Default: 75 12 | - overlayOpacity `` Opacity of the overlayed party color (0 - 100). Default: 60 13 | 14 | Returns `>` 15 | 16 | ## Example 17 | 18 | ### Before 19 | 20 | ![Before](https://i.imgur.com/vdzHTvI.png) 21 | 22 | ### After 23 | 24 | ![AFter](https://i.imgur.com/oIZtzvg.gif) 25 | 26 | ```javascript 27 | const fs = require('fs'); 28 | const partyfy = require('partyfy'); 29 | 30 | (async () => { 31 | const image = fs.readFileSync('my-image.png'); 32 | 33 | const partyImage = await partyfy(image); 34 | 35 | fs.writeFileSync('my-party-image.gif', partyImage); 36 | })(); 37 | ``` 38 | 39 | ## npx/cli 40 | 41 | `npx partyfy` or `npm i partyfy -G` 42 | 43 | ``` 44 | Usage: partyfy [options] 45 | 46 | A CLI for partyfy. 47 | 48 | Options: 49 | -V, --version output the version number 50 | -d, --delay The Speed in milliseconds between frames. (default: 75) 51 | -o, --opacity Opacity of the overlayed party color (0 - 100). (default: 60) 52 | -h, --help output usage information 53 | ``` 54 | -------------------------------------------------------------------------------- /bin/partyfy-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const program = require('commander'); 6 | 7 | const partyfy = require('../dist/partyfy.cjs'); 8 | const pkg = require('../package.json'); 9 | 10 | const main = () => { 11 | try { 12 | program 13 | .version(pkg.version) 14 | .name('partyfy') 15 | .description('A CLI for partyfy.') 16 | .usage(' [options]') 17 | .arguments(' ') 18 | .action(async (source, dest) => { 19 | try { 20 | const imageFile = fs.readFileSync(path.resolve(source)); 21 | 22 | const partyImage = await partyfy(imageFile, { 23 | frameDelay: program.delay, 24 | overlayOpacity: clamp(program.opacity, 100, 0) 25 | }); 26 | 27 | fs.writeFileSync(path.resolve(dest), partyImage); 28 | console.log(`${dest} succesfully created!`); 29 | } catch (err) { 30 | console.log(err); 31 | } 32 | }) 33 | .option( 34 | '-d, --delay ', 35 | 'The Speed in milliseconds between frames.', 36 | 75 37 | ) 38 | .option( 39 | '-o, --opacity ', 40 | 'Opacity of the overlayed party color (0 - 100).', 41 | 60 42 | ) 43 | .parse(process.argv); 44 | } catch (err) { 45 | console.log(err); 46 | } 47 | }; 48 | 49 | main(); 50 | 51 | function clamp(value, max, min) { 52 | return Math.max(min, Math.min(value, max)); 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | # image dirs 96 | input_images/ 97 | output_images/ 98 | 99 | # dist 100 | dist/ 101 | 102 | # .DS_Store 103 | .DS_Store 104 | 105 | # End of https://www.gitignore.io/api/node 106 | -------------------------------------------------------------------------------- /src/partyfy.js: -------------------------------------------------------------------------------- 1 | const getPixels = require('get-pixels'); 2 | const fileType = require('file-type'); 3 | const { GifCodec, GifFrame, GifUtil, BitmapImage } = require('gifwrap'); 4 | 5 | const colors = [ 6 | { r: 255, g: 141, b: 139 }, 7 | { r: 254, g: 214, b: 137 }, 8 | { r: 136, g: 255, b: 137 }, 9 | { r: 135, g: 255, b: 255 }, 10 | { r: 139, g: 181, b: 254 }, 11 | { r: 215, g: 140, b: 255 }, 12 | { r: 255, g: 140, b: 255 }, 13 | { r: 255, g: 104, b: 247 }, 14 | { r: 254, g: 108, b: 183 }, 15 | { r: 255, g: 105, b: 104 }, 16 | ]; 17 | 18 | const defaultOptions = { 19 | overlayOpacity: 60, 20 | frameDelay: 75, 21 | }; 22 | 23 | async function partyfy(imageBuffer, options = defaultOptions) { 24 | try { 25 | const opts = { ...defaultOptions, ...options }; 26 | const frames = await readFrames(imageBuffer, opts); 27 | 28 | frames.forEach(({ bitmap }, frameIdx) => { 29 | for (let idx = 0; idx < bitmap.data.length; idx += 4) { 30 | let pixel = toRGBA(bitmap.data.slice(idx, idx + 5)); 31 | 32 | const { r, g, b, a } = transformPixel( 33 | pixel, 34 | colors[frameIdx % colors.length], 35 | opts.overlayOpacity 36 | ); 37 | 38 | [r, g, b, a].forEach((channel, i) => { 39 | bitmap.data[idx + i] = channel; 40 | }); 41 | } 42 | }); 43 | 44 | const codec = new GifCodec(); 45 | const { buffer } = await codec.encodeGif(frames); 46 | 47 | return buffer; 48 | } catch (err) { 49 | throw new Error(`partyfy error: ${err.message || err}`); 50 | } 51 | } 52 | 53 | // Returns gifwrap/jimp compatible frames 54 | async function readFrames(imageBuffer, opts) { 55 | const { mime } = fileType(imageBuffer); 56 | 57 | switch (mime) { 58 | case 'image/gif': { 59 | const { frames } = await GifUtil.read(imageBuffer); 60 | 61 | if (frames.length > 1) { 62 | if (opts.frameDelay != defaultOptions.frameDelay) { 63 | console.warn( 64 | 'Warning: frameDelay is currently ignored for animated gifs.\n' 65 | ); 66 | } 67 | 68 | return frames; 69 | } else { 70 | return colors.map( 71 | () => 72 | new GifFrame(new BitmapImage(frames[0]), { 73 | delayCentisecs: msToCs(opts.frameDelay), 74 | }) 75 | ); 76 | } 77 | } 78 | case 'image/png': 79 | case 'image/jpg': 80 | case 'image/jpeg': 81 | return new Promise((resolve, reject) => { 82 | try { 83 | getPixels(imageBuffer, mime, (err, { data, shape }) => { 84 | if (err) return reject(err); 85 | 86 | resolve( 87 | colors.map( 88 | () => 89 | new GifFrame( 90 | { 91 | width: shape[0], 92 | height: shape[1], 93 | data: Buffer.from(data), 94 | }, 95 | { delayCentisecs: msToCs(opts.frameDelay) } 96 | ) 97 | ) 98 | ); 99 | }); 100 | } catch (err) { 101 | reject(err); 102 | } 103 | }); 104 | } 105 | 106 | throw new Error('Invalid file format.'); 107 | } 108 | 109 | function toRGBA([r, g, b, a]) { 110 | return { r, g, b, a }; 111 | } 112 | 113 | function setAlpha({ r, g, b, a }) { 114 | return { r, g, b, a: a < 127 ? 0x00 : 255 }; 115 | } 116 | 117 | // Based on luminosity grayscale here: https://docs.gimp.org/2.6/en/gimp-tool-desaturate.html 118 | function grayscale({ r, g, b, a }) { 119 | const grayLevel = parseInt(0.21 * r + 0.72 * g + 0.07 * b, 10); 120 | 121 | return { r: grayLevel, g: grayLevel, b: grayLevel, a }; 122 | } 123 | 124 | function mix(color, overlayedColor, opacity = 60) { 125 | return { 126 | r: (overlayedColor.r - color.r) * (opacity / 100) + color.r, 127 | g: (overlayedColor.g - color.g) * (opacity / 100) + color.g, 128 | b: (overlayedColor.b - color.b) * (opacity / 100) + color.b, 129 | a: color.a, 130 | }; 131 | } 132 | 133 | function transformPixel(pixel, partyColor, opacity = 60) { 134 | return mix(grayscale(setAlpha(pixel)), partyColor, opacity); 135 | } 136 | 137 | function msToCs(ms) { 138 | return ms / 10; 139 | } 140 | 141 | module.exports = partyfy; 142 | --------------------------------------------------------------------------------