Skip to content

Instantly share code, notes, and snippets.

@graup
Last active February 12, 2025 13:51
Show Gist options
  • Select an option

  • Save graup/75c44e975f3baf4f95b6e3f35f0fdb83 to your computer and use it in GitHub Desktop.

Select an option

Save graup/75c44e975f3baf4f95b6e3f35f0fdb83 to your computer and use it in GitHub Desktop.

Revisions

  1. graup renamed this gist Apr 16, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. graup revised this gist Apr 16, 2024. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions Readme.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    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`
  3. graup created this gist Apr 16, 2024.
    3 changes: 3 additions & 0 deletions Readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    1. Install dependencies
    2. `yarn ts-node capture.ts`
    3. `yarn ts-node writeWebP.ts`
    27 changes: 27 additions & 0 deletions capture.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,27 @@
    // 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);
    13 changes: 13 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    {
    "name": "svg-to-webp",
    "author": "Paul Grau <[email protected]>",
    "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"
    }
    }
    51 changes: 51 additions & 0 deletions writeWebP.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,51 @@
    // 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);