Last active
May 17, 2024 02:07
-
-
Save mallendeo/8f589d9e828ebfec287a77be71b6c4d3 to your computer and use it in GitHub Desktop.
Record gsap animations frame by frame with puppeteer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "gsap-to-video", | |
"version": "1.0.0", | |
"main": "index.js", | |
"license": "MIT", | |
"dependencies": { | |
"fs-extra": "^7.0.0", | |
"puppeteer": "^1.7.0" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
// https://github.com/clipisode/puppeteer-recorder/blob/master/index.js | |
const puppeteer = require('puppeteer') | |
const { spawn } = require('child_process') | |
const fs = require('fs-extra') | |
const ANIM_URL = 'https://ipfs.infura.io/ipfs/QmR2gXZz98sQyAtvgK4dGEy8pVXLaKyojcQUe5FVX9WCZu/dist/' | |
const FPS = 60 | |
const WIDTH = 1280 | |
const HEIGHT = 720 | |
const SAVE_IMG = true | |
// x1.5 => 1080p | |
// x3 => 4k | |
// x6 => 8k | |
const SCALE = 1 | |
const getRes = (scale = SCALE) => ({ | |
1: 'hd', | |
'1.5': 'fullhd', | |
3: '4k', | |
6: '8k' | |
})[scale] | |
const filename = () => { | |
if (!getRes()) { | |
throw Error(`Invalid scale, must be one of these: ${Object.keys(res).join()}`) | |
} | |
return `video-${getRes()}.mov` | |
} | |
SAVE_IMG && fs.emptyDir(`./frames-${getRes()}`) | |
const args = [ | |
'-y', | |
'-f', | |
'image2pipe', | |
'-r', | |
`${FPS}`, | |
'-i', | |
'-', | |
'-pix_fmt', | |
'yuv420p', | |
'-crf', | |
'2', | |
filename() | |
] | |
const ffmpeg = spawn('ffmpeg', args) | |
const closed = new Promise((resolve, reject) => { | |
ffmpeg.on('error', reject) | |
ffmpeg.on('close', resolve) | |
}) | |
ffmpeg.stdout.pipe(process.stdout) | |
ffmpeg.stderr.pipe(process.stderr) | |
const write = (stream, buffer) => | |
new Promise((resolve, reject) => { | |
stream.write(buffer, error => { | |
if (error) return reject(error) | |
resolve() | |
}) | |
}) | |
;(async () => { | |
const browser = await puppeteer.launch({ headless: true }) | |
const page = await browser.newPage() | |
await page.setViewport({ | |
width: WIDTH, | |
height: HEIGHT, | |
deviceScaleFactor: SCALE | |
}) | |
await page.goto(ANIM_URL) | |
await page.waitForFunction(() => typeof window.timeline !== 'undefined') | |
const frames = await page.evaluate(async fps => | |
Math.ceil(window.timeline.duration() / 1 * fps) | |
, FPS) | |
let frame = 0 | |
// pause and reset | |
await page.evaluate(() => { | |
window.timeline.pause() | |
window.timeline.progress(0) | |
}) | |
const nextFrame = async () => { | |
await page.evaluate(async progress => { | |
window.timeline.progress(progress) | |
await new Promise(r => setTimeout(r, 16)) | |
}, frame / frames) | |
const filename = (`${frame}`).padStart(6, '0') | |
const opts = SAVE_IMG ? { path: `./frames-${getRes()}/frame${filename}.png` } : undefined | |
const screenshot = await page.screenshot(opts) | |
await write(ffmpeg.stdin, screenshot) | |
frame++ | |
console.log(`frame ${frame} / ${frames}`) | |
if (frame > frames) { | |
console.log('done!') | |
await browser.close() | |
ffmpeg.stdin.end() | |
await closed | |
return | |
} | |
nextFrame() | |
} | |
nextFrame() | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment