├── .eslintrc.json ├── .gitignore ├── README.md ├── main.js └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true 5 | }, 6 | 7 | "parserOptions": { 8 | "ecmaVersion": 2016, 9 | "sourceType": "module" 10 | }, 11 | 12 | "globals": { 13 | "Promise": true 14 | }, 15 | 16 | "rules": { 17 | "no-console" : 0, 18 | "eqeqeq" : ["error", "always"], 19 | "curly" : ["error", "multi-line"], 20 | "indent" : ["error", 2], 21 | "no-undef" : "warn", 22 | "id-length" : ["error", {"min": 1, "max": 40 }], 23 | "valid-typeof" : "warn", 24 | "no-unreachable" : "warn", 25 | "no-unused-vars" : "warn", 26 | "no-const-assign" : "warn", 27 | "constructor-super" : "warn", 28 | "no-this-before-super": "warn" 29 | } 30 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCII Video 2 | 3 | ## What is it 4 | 5 | ASCII Video is a command line tool which allows you to convert movies into ASCII sprite sheets and then play them back in the terminal. It uses [ffmpeg](https://ffmpeg.org/) to break up the video into a series of images, which then are converted to individual ASCII art frames using [image-to-ascii](https://github.com/IonicaBizau/image-to-ascii). It then builds those frames up into a Javascript array as text, and writes the resulting array to an output file that can then be read in by the program and played back in the terminal with the help of [log-update](https://github.com/sindresorhus/log-update). 6 | 7 | ![ASCII Video is Magic](https://www.dropbox.com/s/f92h230quy9xeac/out.gif?dl=0&raw=true 'Demo Video') 8 | 9 | ## Why would you make this 10 | 11 | Because I can. Yes the file size is larger than an mp4. Yes the resolution is terrible. Yes there is no audio. It was just fun to make. 12 | 13 | ## What would I ever use this for 14 | 15 | For me personally, I just use it to make things that make me laugh. If that is not a good enough reason to do something with it, you should use it for: 16 | 17 | * A kick ass loading animation for you CLI tool 18 | * An awesome command line based game with cut scenes 19 | * To show your friends who code who 1337 you are. (That means "[leet](https://www.youtube.com/watch?v=dQw4w9WgXcQ)" which I don't really understand, but I hear it's fun for young people.) 20 | * Put all of your hippest friends to shame when you tell them you don't watch movies anymore, but instead you download text files that are 50 times larger than a regular movie file that you can only watch in a Unix type command line environment. They won't know what you're talking about, but rest assured, they will think you are super cool. 21 | 22 | ## How do I get it (installation) 23 | 24 | 1. If you're on a Mac, the easiest way to get the initial dependencies is through [Homebrew](https://brew.sh/). If you're not on a Mac then you'll have to figure out how to load the initial dependencies on your machine, but everything after installing Node and FFMPEG should be the same. 25 | 26 | 1. Next you need to install [ffmpeg](https://ffmpeg.org/) by running: 27 | ```bash 28 | brew update && brew install ffmpeg 29 | ``` 30 | 1. If you do not have [node](https://nodejs.org/en/), install it by running: 31 | ```bash 32 | brew update && brew install node 33 | ``` 34 | 1. Node comes with a package manager called NPM. To install [ASCII-Video](https://github.com/fossage/ASCII-Video) run: 35 | 36 | ```bash 37 | npm install ascii-video 38 | ``` 39 | 40 | Alternatively, you can just simply clone the repo from github, and then install the projects dependencies by navigating to the cloned repo and running: 41 | 42 | ```bash 43 | npm install 44 | ``` 45 | 46 | 1. Next, navigate to the ASCII-Video directory within the node_modules directory and run: 47 | ```bash 48 | npm link 49 | ``` 50 | This will allow you to run the program the same way you would any other bash program from anywhere on the machine. 51 | 52 | ## How do I use it 53 | 54 | There are two commands that you use with `ascii-vido`; the `create` command and the `play` command. 55 | 56 | ### The `create` command 57 | 58 | ```bash 59 | ascii-video create <[path/to/]input-video> <[path/to/]ouput-filename.yaml> 60 | ``` 61 | 62 | I.E. 63 | 64 | ```bash 65 | ascii-video create ~/Documents/foo.mov ~/Documents/ascii-video-output/foo.yaml 66 | ``` 67 | 68 | Both the input video and output filename are required. Note that is should be able to convert most all video formats so there is no strict specification. Both the input video and output filename can be preceeded with a relative or absolute path, but if any path is ommitted, then the program will assume you are referring to the current working directory. 69 | 70 | One other thing to note is that the ouput filename MUST be a YAML file(ends with the .yaml extension). 71 | 72 | Lastly, note that currently the way the program will figure out what size to make the video is by determining the size of your terminal window when the command is run. If you would like a larger, more detailed video, simply make your terminal window larger and vice-versa. 73 | 74 | ### The `play` command 75 | 76 | ```bash 77 | ascii-video play <[path/to/]filename> [ --frame_rate ] 78 | ``` 79 | 80 | I.E. 81 | 82 | ```bash 83 | ascii-video play ~/Documents/ascii-video-output/foo.js --frame_rate 20 84 | ``` 85 | 86 | This command will play back a YAML file that was created with the `create` command. You will likely want to use the `--frame_rate` flag, or just `-f` for shorthand, because I have found that different videos seem to vary pretty widely on what seems like an acceptable framerate. It is defaulted to 155, but I will often find myself running it as low as 5 for some videos and as much as 200 for others. Just play around until you find a setting for your particular video that works. 87 | 88 | Also, as mentioned in the create section, the size of the video is determined by the size of your terminal window at the time when you run the `create` command, so to ensure that it plays back in the correctly sized viewport, it is best to run `play` in the same terminal window(or at least on of the same size/aspect ratio) as the one in which the input file was created. 89 | 90 | ## @TODO: 91 | 92 | * Add option flags to `create` to allow user to specify width and height of video 93 | * Add flag to `create` to allow user to turn color encoding on/off 94 | * Add flag to `play` command to toggle looping on/off 95 | 96 | If you have any other improvements you would like to see, please feel free to submit a feature request. Also, if you make any neat projects using this, please let me know so I can feature them on the README. Thanks! 97 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --harmony 2 | 3 | /*===================================================== 4 | IMPORTS / SETUP 5 | ======================================================*/ 6 | const _ = require('lodash') 7 | const fs = require('fs') 8 | const yaml = require('js-yaml') 9 | const clc = require('cli-color') 10 | const shell = require('shelljs') 11 | const program = require('commander') 12 | const logUpdate = require('log-update') 13 | const imageToAscii = require('image-to-ascii') 14 | 15 | const TMP_DIR_PATH = '/tmp/__sprite_cli_output/' 16 | const END_OF_FRAME_ID = '\nzzzzzzzzzzzzzzzzzzzzzzz' 17 | /*===================================================== 18 | MAIN 19 | ======================================================*/ 20 | program 21 | .version('0.1.2') 22 | .command('create ') 23 | .description( 24 | 'Takes an input video, converts it into ASCII frames, and writes it to an output file.' 25 | ) 26 | .action((video, outputTo) => { 27 | if (!_.endsWith(outputTo, '.yaml')) { 28 | return console.log(errMsg('The outputfile must be a yaml file.')) 29 | } 30 | 31 | // remove the output file if it exists already 32 | if (fs.existsSync(outputTo)) { 33 | fs.unlinkSync(outputTo) 34 | } 35 | 36 | const dir = process.cwd() 37 | const finishLoadingId = showLoading() 38 | 39 | // make temp directory to write image files to 40 | shell.exec(`cd /tmp && mkdir __sprite_cli_output && cd ${dir}`) 41 | 42 | if ( 43 | shell.exec(`ffmpeg -i ${program.args[0]} ${TMP_DIR_PATH}image%d.jpg`) 44 | .code !== 0 45 | ) { 46 | // stop loading animation 47 | clearInterval(finishLoadingId) 48 | return console.log(errMsg('@todo: error message for shit went wrong.')) 49 | } 50 | 51 | // stop loading animation 52 | clearInterval(finishLoadingId) 53 | 54 | // ensure frames are in correct order 55 | const files = [] 56 | fs.readdirSync(TMP_DIR_PATH) 57 | .forEach(f => { 58 | try { 59 | const fId = parseInt(f.match(/\d/g).join('')) 60 | files[fId - 1] = f // convert 1-indexed id to 0-indexed 61 | } catch (e) { 62 | return 0 63 | } 64 | }) 65 | 66 | createSprites(files, outputTo, 0, []) 67 | }) 68 | 69 | program 70 | .command('play ') 71 | .description('Plays back a generated sprite file') 72 | .option( 73 | '-f, --frame_rate ', 74 | 'A number which specifies the rate at which to iterate through the sprites' 75 | ) 76 | .action((pathToFile, opts) => { 77 | console.log(pathToFile) 78 | const lineReader = require('readline').createInterface({ 79 | input: require('fs').createReadStream(pathToFile), 80 | }) 81 | 82 | let frame = '' 83 | const re = RegExp(END_OF_FRAME_ID, 'g') 84 | const frameRate = opts && opts.frame_rate ? opts.frame_rate : 155 85 | 86 | lineReader.on('line', async line => { 87 | lineReader.pause() 88 | const fragment = yaml.safeLoad(line)[0] 89 | frame += fragment 90 | 91 | if (re.test(frame)) { 92 | const frames = frame.split(END_OF_FRAME_ID) 93 | for (let frameItem of frames) { 94 | await delay(frameRate) 95 | if (frameItem.length) logUpdate(frameItem) 96 | } 97 | 98 | frame = '' 99 | } 100 | 101 | lineReader.resume() 102 | }) 103 | }) 104 | 105 | program.parse(process.argv) 106 | if (!program.args.length) program.help() 107 | 108 | function delay(time) { 109 | return new Promise(resolve => setTimeout(resolve, time)) 110 | } 111 | 112 | /*===================================================== 113 | HELPERS 114 | ======================================================*/ 115 | function createSprites(files, outputTo, idx, sprites) { 116 | if (idx === files.length) { 117 | appendToFile(outputTo, sprites, true) 118 | // clean up temp directory after the last chunk of sprites is written 119 | shell.exec(`rm -rf ${TMP_DIR_PATH}`) 120 | console.log(infoMsg(`File written to ${outputTo}`)) 121 | } else { 122 | imageToAscii( 123 | TMP_DIR_PATH + files[idx], 124 | { 125 | image_type: 'jpg', 126 | }, 127 | (err, converted) => { 128 | if (err) { 129 | console.log(warningMsg(err)) 130 | } else { 131 | sprites.push(converted + '\nzzzzzzzzzzzzzzzzzzzzzzz') 132 | 133 | // write to disk before sprites array gets too large 134 | if (sprites.length > 500) { 135 | appendToFile(outputTo, sprites) 136 | sprites = [] 137 | } 138 | 139 | logUpdate( 140 | `Creating sprites: ${Math.round(idx / files.length * 100)}%` 141 | ) 142 | } 143 | 144 | createSprites(files, outputTo, idx + 1, sprites) 145 | } 146 | ) 147 | } 148 | } 149 | 150 | function appendToFile(outputTo, sprites) { 151 | const outFile = yaml.safeDump(sprites) 152 | 153 | fs.appendFile(outputTo, outFile, err => { 154 | if (err) return console.log(warningMsg(err)) 155 | }) 156 | } 157 | 158 | function showLoading() { 159 | const frames = ['-', '\\', '|', '/'] 160 | let i = 0 161 | 162 | return setInterval(() => { 163 | const frame = frames[(i = ++i % frames.length)] 164 | logUpdate(`${frame} Converting video to frames ${frame}`) 165 | }, 80) 166 | } 167 | 168 | function readFile(pathTo) { 169 | return new Promise((resolve, reject) => { 170 | fs.readFile(pathTo, (err, data) => { 171 | if (!err) return resolve(data) 172 | console.log(err) 173 | reject(err) 174 | }) 175 | }) 176 | } 177 | 178 | function infoMsg(msg) { 179 | const infoColor = clc.xterm(33) 180 | return infoColor(msg) 181 | } 182 | 183 | function errMsg(msg) { 184 | const errColor = clc.xterm(9) 185 | return errColor(msg) 186 | } 187 | 188 | function warningMsg(msg) { 189 | const warningColor = clc.xterm(214) 190 | return warningColor(msg) 191 | } 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ascii-video", 3 | "version": "0.1.3", 4 | "description": "A tool to create and play sprite animation in the terminal", 5 | "main": "./main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/fossage/ASCII-Video" 9 | }, 10 | "maintainers": [ 11 | { 12 | "name": "fossage", 13 | "email": "jfoss124@gmail.com" 14 | } 15 | ], 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "bin": { 20 | "ascii-video": "./main.js" 21 | }, 22 | "author": "Justin Foss", 23 | "contributors": [{ "name": "Xinyi Chen" }, { "name": "Jeremie Zarca" }], 24 | "license": "ISC", 25 | "dependencies": { 26 | "cli-color": "^1.2.0", 27 | "commander": "^2.9.0", 28 | "gm": "^1.23.0", 29 | "image-to-ascii": "^3.0.5", 30 | "js-yaml": "^3.10.0", 31 | "lodash": "^4.17.4", 32 | "log-update": "^1.0.2", 33 | "shelljs": "^0.7.7" 34 | }, 35 | "engines": { 36 | "node": ">=6.10.0", 37 | "npm": ">=4.0.5" 38 | } 39 | } 40 | --------------------------------------------------------------------------------