Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
transitive-bullshit committed May 10, 2019
0 parents commit 60bd3dd
Show file tree
Hide file tree
Showing 12 changed files with 9,046 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
5 changes: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": [
"standard"
]
}
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
node_modules

# builds
build
dist

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.cache

lerna-debug.log*
lerna-error.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
language: node_js
cache: yarn
node_js:
- 10
- 8
1 change: 1 addition & 0 deletions fixtures/bodymovin.json

Large diffs are not rendered by default.

274 changes: 274 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
'use strict'

const fs = require('fs-extra')
const execa = require('execa')
const ow = require('ow')
const path = require('path')
const puppeteer = require('puppeteer')
const tempy = require('tempy')
const util = require('util')

const { cssifyObject } = require('css-in-js-utils')

const lottieScript = fs.readFileSync(path.join(__dirname, 'lib', 'lottie.min.js'), 'utf8')

const injectLottie = `
<script>
${lottieScript}
</script>
`

/**
* Renders the given Lottie animation via Puppeteer.
*
* Must pass either `path` or `animationData`.
*
* @name renderLottie
* @function
*
* @param {object} opts - Configuration options
* @param {string} opts.output - Path or pattern to store result
* @param {object} [opts.animationData] - JSON exported animation data
* @param {string} [opts.path] - Relative path to the animation object
* @param {number} [opts.width] - Optional output width
* @param {number} [opts.height] - Optional output height
* @param {number} [opts.deviceScaleFactor=1] - Window device scale factor
* @param {string} [opts.renderer='svg'] - Which lottie-web renderer to use
* @param {object} [opts.rendererSettings] - Optional lottie renderer options
* @param {object} [opts.puppeteerOptions] - Optional puppeteer launch options
* @param {object} [opts.style={}] - Optional JS [CSS styles](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference) to apply to the animation container
* @param {object} [opts.inject={}] - Optionally injects arbitrary string content into the head, style, or body elements.
* @param {string} [opts.inject.head] - Optionally injected into the document <head>
* @param {string} [opts.inject.style] - Optionally injected into a <style> tag within the document <head>
* @param {string} [opts.inject.body] - Optionally injected into the document <body>
*
* @return {Promise}
*/
module.exports = async (opts) => {
const {
output,
animationData = undefined,
path: animationPath = undefined,
deviceScaleFactor = 1,
renderer = 'svg',
rendererSettings = { },
style = { },
inject = { },
puppeteerOptions = { },
jpgQuality = 90,
gifskiOptions = {
fps: 10,
quality: 80,
fast: false
}
} = opts

let {
width = undefined,
height = undefined
} = opts

ow(output, ow.string.nonEmpty, 'output')
ow(deviceScaleFactor, ow.number.integer.positive, 'deviceScaleFactor')
ow(renderer, ow.string.oneOf([ 'svg', 'canvas', 'html' ], 'renderer'))
ow(rendererSettings, ow.object.plain, 'rendererSettings')
ow(puppeteerOptions, ow.object.plain, 'puppeteerOptions')
ow(style, ow.object.plain, 'style')
ow(inject, ow.object.plain, 'inject')

const ext = path.extname(output).slice(1).toLowerCase()
const isGif = (ext === 'gif')
const isMp4 = (ext === 'mp4')
const isPng = (ext === 'png')
const isJpg = (ext === 'jpg' || ext === 'jpeg')

if (!(isGif || isMp4 || isPng || isJpg)) {
throw new Error(`Unsupported output format "${output}"`)
}

const tempDir = isGif ? tempy.directory() : undefined
const tempOutput = isGif
? path.join(tempDir, 'frame-%012d.png')
: output
const frameType = (isJpg ? 'jpeg' : 'png')
const isMultiFrame = isMp4 || /%d|%\d{2,3}d/.test(tempOutput)

let lottieData = animationData

if (animationPath) {
if (animationData) {
throw new Error('"animationData" and "path" are mutually exclusive')
}

ow(animationPath, ow.string.nonEmpty, 'path')

lottieData = fs.readJsonSync(animationPath)
} else if (animationData) {
ow(animationData, ow.object.plain.nonEmpty, 'animationData')
} else {
throw new Error('Must pass either "animationData" or "path"')
}

const fps = lottieData.fr
const { w, h } = lottieData
const aR = w / h

ow(fps, ow.number.integer.positive, 'animationData.fr')
ow(w, ow.number.integer.positive, 'animationData.w')
ow(h, ow.number.integer.positive, 'animationData.h')

if (width) {
height = width / aR
} else if (height) {
width = height * aR
} else {
width = w
height = h
}

const html = `
<html>
<head>
<meta charset="UTF-8">
${inject.head || ''}
${injectLottie}
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: transparent;
${width ? 'width: ' + width + 'px;' : ''}
${height ? 'height: ' + height + 'px;' : ''}
overflow: hidden;
}
#root {
${cssifyObject(style)}
}
${inject.style || ''}
</style>
</head>
<body>
${inject.body || ''}
<div id="root"></div>
<script>
const animationData = ${JSON.stringify(lottieData)};
let animation = null;
let duration;
let numFrames;
function onReady () {
animation = lottie.loadAnimation({
container: document.getElementById('root'),
renderer: '${renderer}',
loop: false,
autoplay: false,
rendererSettings: ${JSON.stringify(rendererSettings)},
animationData,
});
duration = animation.getDuration();
numFrames = animation.getDuration(true);
var div = document.createElement('div');
div.className = 'ready';
document.body.appendChild(div);
}
document.addEventListener('DOMContentLoaded', onReady);
</script>
</body>
</html>
`

// useful for testing purposes
// fs.writeFileSync('test.html', html)

const browser = opts.browser || await puppeteer.launch({
...puppeteerOptions
})
const page = await browser.newPage()

page.on('console', console.log)
page.on('error', console.error)

await page.setViewport({
deviceScaleFactor,
width,
height
})
await page.setContent(html)
await page.waitForSelector('.ready')
const duration = await page.evaluate(() => duration)
const numFrames = await page.evaluate(() => numFrames)

const pageFrame = page.mainFrame()
const rootHandle = await pageFrame.$('#root')

const screenshotOpts = {
omitBackground: true,
type: frameType,
quality: frameType === 'jpeg' ? jpgQuality : undefined
}

for (let frame = 1; frame <= numFrames; ++frame) {
const frameOutputPath = isMultiFrame
? util.format(tempOutput, frame)
: tempOutput

// eslint-disable-next-line no-undef
await page.evaluate((frame) => animation.goToAndStop(frame, true), frame)
const screenshot = await rootHandle.screenshot({
path: isMp4 ? undefined : frameOutputPath,
...screenshotOpts
})

// single screenshot
if (!isMultiFrame) {
break
}

if (isMp4) {
// TODO
}
}

await rootHandle.dispose()
await browser.close()

if (isGif) {
const framePattern = tempOutput.replace('%012d', '*')
const escapePath = arg => arg.replace(/(\s+)/g, '\\$1')

const params = [
'-o', escapePath(output),
'--fps', gifskiOptions.fps,
gifskiOptions.fast && '--fast',
'--quality', gifskiOptions.quality,
'--quiet',
escapePath(framePattern)
].filter(Boolean)

const executable = process.env.GIFSKI_PATH || 'gifski'
const cmd = [ executable ].concat(params).join(' ')

console.log(cmd)
await execa.shell(cmd)
await fs.remove(tempDir)
}

return html
}
42 changes: 42 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict'

const fs = require('fs-extra')
const test = require('ava')
const sharp = require('sharp')
const tempy = require('tempy')

const renderLottie = require('.')

test('bodymovin.json single frame png', async (t) => {
const output0 = tempy.file({ extension: 'png' })

await renderLottie({
path: 'fixtures/bodymovin.json',
output: output0
})

const image0 = await sharp(output0).metadata()
t.is(image0.width, 1820)
t.is(image0.height, 275)
t.is(image0.channels, 4)
t.is(image0.format, 'png')

await fs.remove(output0)
})

test.only('bodymovin.json single frame jpg', async (t) => {
const output0 = tempy.file({ extension: 'jpg' })

await renderLottie({
path: 'fixtures/bodymovin.json',
output: output0
})

const image0 = await sharp(output0).metadata()
t.is(image0.width, 1820)
t.is(image0.height, 275)
t.is(image0.channels, 3)
t.is(image0.format, 'jpeg')

await fs.remove(output0)
})
8 changes: 8 additions & 0 deletions lib/fontfaceobserver.standalone.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/lottie.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit 60bd3dd

Please sign in to comment.