import { Buffer } from "node:buffer"; import { readFile, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import { cwd, stdout } from "node:process"; import { URL } from "node:url"; import { chromium } from "@playwright/test"; import { build } from "esbuild"; const require = createRequire(import.meta.url); const riveRequire = createRequire(require.resolve("@rive-app/react-webgl2")); const wasm = await readFile(riveRequire.resolve("@rive-app/webgl2/rive.wasm")); const resource = await readFile(new URL("../public/agent/obsidian-water-v1.riv", import.meta.url)); const harness = `import { RuntimeLoader } from '@rive-app/react-webgl2'; RuntimeLoader.setWasmUrl('/runtime.wasm'); window.stills = new Promise(resolve => RuntimeLoader.getInstance(resolve)).then(async runtime => { const bytes = new Uint8Array(await (await fetch('/resource.riv')).arrayBuffer()); const file = await runtime.load(bytes, undefined, false); const canvas = document.querySelector('canvas'); const renderer = runtime.makeRenderer(canvas, true); const images = {}; try { for (const state of ['idle', 'asleep']) { const artboard = file.defaultArtboard(); const machine = new runtime.StateMachineInstance(artboard.stateMachineByName('default'), artboard); try { for (let i = 0; i < machine.inputCount(); i++) { const input = machine.input(i); input.asBool().value = input.name === state; } for (let frame = 0; frame < (state === 'idle' ? 90 : 180); frame++) { machine.advance(1 / 60); artboard.advance(1 / 60); } renderer.clear(); renderer.save(); renderer.align(runtime.Fit.contain, runtime.Alignment.center, { minX: 0, minY: 0, maxX: 192, maxY: 192 }, artboard.bounds); artboard.draw(renderer); renderer.restore(); renderer.flush(); runtime.resolveAnimationFrame(); images[state] = canvas.toDataURL('image/png'); } finally { machine.delete(); artboard.delete(); } } return images; } finally { renderer.delete(); file.delete(); } });`; const bundled = await build({ stdin: { contents: harness, resolveDir: cwd() }, bundle: true, write: false, format: "iife" }); const browser = await chromium.launch({ args: ["--enable-unsafe-swiftshader"] }); try { const page = await browser.newPage(); await page.route("http://persona-render.local/**", async (route) => { const path = new URL(route.request().url()).pathname; if (path.endsWith(".wasm")) return route.fulfill({ body: wasm, contentType: "application/wasm" }); if (path.endsWith(".riv")) return route.fulfill({ body: resource }); if (path.endsWith(".js")) return route.fulfill({ body: bundled.outputFiles[0].text, contentType: "application/javascript" }); return route.fulfill({ contentType: "text/html", body: '' }); }); await page.goto("http://persona-render.local/"); const images = await page.evaluate("window.stills"); for (const [state, image] of Object.entries(images)) { const name = state === "asleep" ? "offline" : state; const path = new URL(`../public/agent/obsidian-water-${name}.png`, import.meta.url); await writeFile(path, Buffer.from(image.split(",")[1], "base64")); } stdout.write("Rendered current Rive material into online and ring-free offline fallback images.\n"); } finally { await browser.close(); }