Skip to content

Instantly share code, notes, and snippets.

@mallendeo
Last active July 10, 2023 13:46
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save mallendeo/8f589d9e828ebfec287a77be71b6c4d3 to your computer and use it in GitHub Desktop.
Save mallendeo/8f589d9e828ebfec287a77be71b6c4d3 to your computer and use it in GitHub Desktop.
Record gsap animations frame by frame with puppeteer
{
"name": "gsap-to-video",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"fs-extra": "^7.0.0",
"puppeteer": "^1.7.0"
}
}
'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