Skip to content

Instantly share code, notes, and snippets.

@graup
Last active April 16, 2024 12:00
Show Gist options
  • Save graup/75c44e975f3baf4f95b6e3f35f0fdb83 to your computer and use it in GitHub Desktop.
Save graup/75c44e975f3baf4f95b6e3f35f0fdb83 to your computer and use it in GitHub Desktop.
Convert animated SVG into animated WEBP

We use playwright to capture screenshots of our animated SVG (or really anything that can be loaded into a browser), then use sharp to convert pngs into webps, then node-webpmux to create the animated webp.

  1. Install dependencies
  2. yarn ts-node capture.ts
  3. yarn ts-node writeWebP.ts
// Convert svg into frames by capturing screenshots with Playwright
import { chromium, Browser, Page } from 'playwright';
const dir = 'frames/';
async function captureFrames(url: string, totalFrames: number, frameDurationMs: number): Promise<void> {
const browser: Browser = await chromium.launch();
const page: Page = await browser.newPage();
await page.goto(url);
const svg = page.locator('svg');
const boundingBox = await svg.boundingBox();
for (let i = 0; i < totalFrames; i++) {
await page.screenshot({ path: `${dir}frame${i}.png`, clip: boundingBox });
await page.waitForTimeout(frameDurationMs);
}
await browser.close();
}
const url = "file:///path to your svg";
const duration = 2000;
const fps = 20;
const totalFrames = (duration / 1000) * fps;
captureFrames(url, totalFrames, duration / totalFrames);
{
"name": "svg-to-webp",
"author": "Paul Grau <paul@graycoding.com>",
"license": "ISC",
"dependencies": {
"@types/node": "^20.12.7",
"node-webpmux": "^3.2.0",
"playwright": "^1.43.1",
"sharp": "^0.33.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}
// Make animated webp file from individual frames
// @ts-ignore: missing type
import { Image } from 'node-webpmux';
import * as sharp from 'sharp';
import { promises as fs } from 'fs';
async function getOriginalFrames(inputDirectory: string) {
const filePaths = await fs.readdir(inputDirectory).then(files =>
files.flatMap(file => file.endsWith('.png') ? `${inputDirectory}/${file}` : [])
);
filePaths.sort((a, b) => a.localeCompare(b, 'en', { numeric: true, ignorePunctuation: true }));
return filePaths;
}
async function convertToWebP(inputFilePaths: string[]) {
return await Promise.all(inputFilePaths.map(async filePath => {
const webpFile = `${filePath}.webp`;
await sharp(filePath).toFile(webpFile);
return webpFile;
}));
}
async function createWebPAnimation(inputDirectory: string, outputPath: string, delay: number): Promise<void> {
const originFilePaths = await getOriginalFrames(inputDirectory);
const framePaths = await convertToWebP(originFilePaths);
await Image.initLib();
const firstFrame = new Image();
await firstFrame.load(await fs.readFile(framePaths[0]));
firstFrame.convertToAnim();
const frames = await Promise.all(
framePaths.map(async (path) => {
const frame = new Image();
await frame.load(await fs.readFile(path));
return frame;
})
);
await firstFrame.save(outputPath, {
frames: frames.map(frame => ({
img: frame,
delay,
}))
});
}
createWebPAnimation("frames", 'animation.webp', 100);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment