├── .gitignore ├── README.md ├── package.json └── puppeteer-gif-cast.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # package-lock.json not needed 64 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puppeteer GIF Cast 2 | 3 | This little application was written out of necessity for writing documentation about UX/UI. All it does is starts a headless instance of puppeteer, loads the url passed to it, and scrolls the page taking screenshots. At the end it will stitch all the screenshots into one gif as if it was a screencast. 4 | 5 | This utility expects two mandatory arguments: 6 | * **URL:** the url of the website you want to screencast 7 | * **NAME:** the name of the gif (.gif will be automatically appended at the end) 8 | 9 | Other arguments that can also be passed to control the gif generation: 10 | * Width: defaults to 768 11 | * Height: defaults to 600 12 | * Duration: how long to capture the gif for. Defaults to 60s 13 | * Scroll: How far down to scroll the page per frame, in pixels. Defaults to 100. Higher numbers means the page will scroll farther per frame, which can be useful for long pages or pages that make heavy use of the browser scroll position. 14 | 15 | 16 | GIFs are saved in the ./gifs/ folder at the end of the process. 17 | 18 | ## How to use it 19 | clone the repository to your machine: 20 | 21 | `git clone https://github.com/aimerib/puppeteer-gif-cast.git` 22 | 23 | `cd puppeteer-gif-cast` 24 | 25 | initialize the project 26 | 27 | `npm install` 28 | 29 | to grab a screencast of a website run the command like this: 30 | 31 | `npm start http://everylastdrop.co.uk every-last-drop -- --scroll 500` 32 | 33 | the generated gif will be located at ./gifs/every-last-drop.gif 34 | 35 | ![alt text](https://raw.githubusercontent.com/aimerib/aimerib.github.io/master/images/every-last-drop.gif "UK's Every Last Drop website screencast") 36 | 37 | 38 | the scroll parameter can be ommited safely like so: 39 | 40 | `npm start http://everylastdrop.co.uk every-last-drop` 41 | 42 | ![alt text](https://raw.githubusercontent.com/aimerib/aimerib.github.io/master/images/javascript.gif "Wikipedia's Javascript article") 43 | 44 | ***Important*** 45 | 46 | To ensure that optional parameters are sent correctly to the node process you MUST pass them after `--` 47 | Please see examples below 48 | 49 | ## Help 50 | ``` 51 | npm start 52 | 53 | starts capturing the gif at and save it with .gif 54 | 55 | Positionals: 56 | url the url to caputre a scrolling gif from [string] 57 | name the name of the final gif file [string] 58 | 59 | Options: 60 | --help Show help [boolean] 61 | --version Show version number [boolean] 62 | -w, --width Sets a width for the captured gif [number] [default: 768] 63 | -h, --height Sets a height for the captured gif [number] [default: 600] 64 | -s, --scroll How far to scroll down the per frame [number] [default: 100] 65 | -d, --duration Sets gif duration in seconds [number] [default: 60] 66 | ``` 67 | ### Examples: 68 | ```bash 69 | # capture gif using default values and saves it as every_last_drop.gif 70 | npm start http://everylastdrop.co.uk every_last_drop 71 | ``` 72 | 73 | ```bash 74 | # capture gif with custom dimensions 75 | npm start http://everylastdrop.co.uk every_last_drop -- --width 1080 --height 720 76 | ``` 77 | 78 | ```bash 79 | # capture gif for 120 seconds 80 | npm start http://everylastdrop.co.uk every_last_drop -- --duration 120 81 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-gif-cast", 3 | "version": "1.1.0", 4 | "description": "Creates screencasting-like gifs of page-scrolls", 5 | "main": "puppeteer-gif-cast.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node puppeteer-gif-cast.js" 9 | }, 10 | "keywords": [], 11 | "author": "Aimeri Baddouh", 12 | "license": "MIT", 13 | "dependencies": { 14 | "get-pixels": "^3.3.0", 15 | "gif-encoder": "^0.7.2", 16 | "puppeteer": "^16.1.0", 17 | "yargs": "^17.5.1" 18 | } 19 | } -------------------------------------------------------------------------------- /puppeteer-gif-cast.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //A little setup ahead of time 4 | const yargs = require('yargs/yargs') 5 | const { hideBin } = require('yargs/helpers') 6 | 7 | let args = yargs(hideBin(process.argv)) 8 | .usage('$0 ', 'starts capturing the gif at and save it with .gif', (yargs) => { 9 | yargs.positional('url', { 10 | describe: 'the url to caputre a scrolling gif from', 11 | type: 'string' 12 | }) 13 | .positional('name', { 14 | describe: "the name of the final gif file", 15 | type: "string" 16 | }) 17 | }) 18 | .option('width', { 19 | alias: 'w', 20 | type: 'number', 21 | description: 'Sets a width for the captured gif', 22 | default: 768 23 | }) 24 | .option('height', { 25 | alias: 'h', 26 | type: 'number', 27 | description: 'Sets a height for the captured gif', 28 | default: 600 29 | }) 30 | .option('scroll', { 31 | alias: 's', 32 | type: 'number', 33 | description: 'How far to scroll down the per frame', 34 | default: 100 35 | }) 36 | .option('duration', { 37 | alias: 'd', 38 | type: 'number', 39 | description: 'Sets gif duration in seconds', 40 | default: 60 41 | }) 42 | .example([ 43 | ['npm start http://example.com example', 'capture gif using default values and saves it as example.gif'], 44 | ['npm start http://example.com example -- --width 1080 --height 720', 'capture gif with custom dimensions'], 45 | ['npm start http://example.com example -- --duration 120', 'capture gif for 120 seconds'] 46 | ]) 47 | .parse() 48 | 49 | const { width, height, name, duration, url, scroll } = args; 50 | 51 | const puppeteer = require('puppeteer'); 52 | const GIFEncoder = require('gif-encoder'); 53 | const fs = require('fs'); 54 | const getPixels = require('get-pixels'); 55 | const workDir = './temp/'; 56 | const gifDir = './gifs/'; 57 | 58 | if (!fs.existsSync(workDir)) { 59 | fs.mkdirSync(workDir); 60 | }; 61 | 62 | if (!fs.existsSync(gifDir)) { 63 | fs.mkdirSync(gifDir); 64 | }; 65 | 66 | let file = require('fs').createWriteStream(gifDir + name + '.gif'); 67 | 68 | 69 | 70 | // Setup gif encoder and parameters 71 | const encoder = new GIFEncoder(width, height); 72 | encoder.setFrameRate(60); 73 | encoder.pipe(file); 74 | encoder.setQuality(40); 75 | encoder.setDelay(500); 76 | encoder.writeHeader(); 77 | encoder.setRepeat(0); 78 | 79 | // Function Declarations 80 | function addToGif(images, counter = 0) { 81 | getPixels(images[counter], function (err, pixels) { 82 | 83 | encoder.addFrame(pixels.data); 84 | encoder.read(); 85 | if (counter === images.length - 1) { 86 | encoder.finish(); 87 | cleanUp(images, function (err) { 88 | if (err) { 89 | console.log(err); 90 | } else { 91 | fs.rmdirSync(workDir); 92 | console.log('Gif created!'); 93 | process.exit(0); 94 | } 95 | }); 96 | 97 | } else { 98 | addToGif(images, ++counter); 99 | } 100 | }) 101 | } 102 | 103 | 104 | 105 | function cleanUp(listOfPNGs, callback) { 106 | let i = listOfPNGs.length; 107 | listOfPNGs.forEach(function (filepath) { 108 | fs.unlink(filepath, function (err) { 109 | i--; 110 | if (err) { 111 | callback(err); 112 | return; 113 | } else if (i <= 0) { 114 | callback(null); 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | // This is where the magic happens: 121 | (async () => { 122 | console.info(`Capturing gif "${name}.gif" with following parameters:`) 123 | console.info(`Origin URL: ${url}`) 124 | console.info(`Width: ${width}`) 125 | console.info(`Height: ${height}`) 126 | console.info(`Duration: ${duration}`) 127 | console.info(`Scroll length: ${scroll}\n`) 128 | 129 | const browser = await puppeteer.launch({ headless: true }); 130 | const page = await browser.newPage(); 131 | 132 | 133 | 134 | await page.setViewport({ width: width, height: height }); 135 | await page.goto(url); 136 | 137 | async function scrollPage() { 138 | await page.evaluate(async (scrollLength) => { 139 | window.scrollBy(0, scrollLength); 140 | }, scroll); 141 | } 142 | 143 | for (let i = 0; i < duration; i++) { 144 | await page.screenshot({ path: workDir + i + ".png" }); 145 | await scrollPage(); 146 | } 147 | 148 | await browser.close(); 149 | 150 | /* Creates array of pngs by listing files inside export folder then 151 | removing extention, sort numerical strings in ascending order, and 152 | finally adding path and extention to the file. */ 153 | let listOfPNGs = fs.readdirSync(workDir) 154 | .map(a => a.substr(0, a.length - 4) + '') 155 | .sort(function (a, b) { return a - b }) 156 | .map(a => workDir + a.substr(0, a.length) + '.png'); 157 | 158 | addToGif(listOfPNGs); 159 | })(); 160 | 161 | --------------------------------------------------------------------------------