UNPKG

@frames.js/debugger

Version:

Debug your Frames.js applications

200 lines (179 loc) 4.96 kB
import type { ImageAspectRatio } from "frames.js"; import satori, { init } from "satori"; import initYoga from "yoga-wasm-web"; let yogaInitialized = false; async function initializeYoga() { if (yogaInitialized) { return; } const yoga = await initYoga( await fetch("/yoga.wasm", { cache: "force-cache" }).then((res) => res.arrayBuffer() ) ); init(yoga); yogaInitialized = true; } async function loadFont({ family, weight, text, }: { family: string; weight?: number; text?: string; }): Promise<ArrayBuffer> { const searchParams = new URLSearchParams(); searchParams.set("family", family); if (weight) { searchParams.set("weight", weight.toString()); } if (text) { searchParams.set("text", text); } return fetch(`/font?${searchParams.toString()}`).then((res) => res.arrayBuffer() ); } export async function renderImage( element: React.ReactElement, aspectRatio: ImageAspectRatio = "1.91:1" ): Promise<string> { await initializeYoga(); const svgContent = await satori( { // taken from frames.js renderResponse middleware type: "div", key: "", props: { style: { display: "flex", // Use flex layout flexDirection: "row", // Align items horizontally alignItems: "stretch", // Stretch items to fill the container height width: "100%", height: "100vh", // Full viewport height backgroundColor: "white", }, children: [ { type: "div", key: "", props: { style: { display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", lineHeight: 1.2, fontSize: 36, color: "black", flex: 1, overflow: "hidden", }, children: element, }, }, ], }, }, { ...(aspectRatio === "1:1" ? { width: 1146, height: 1146, } : { width: 1146, height: 600, }), fonts: [ { name: "Inter", data: await loadFont({ family: "Inter", weight: 400 }), weight: 400, }, { name: "Inter", data: await loadFont({ family: "Inter", weight: 700 }), weight: 700, }, ], embedFont: true, async loadAdditionalAsset(code, text) { if (code === "emoji") { return ( "data:image/svg+xml;base64," + btoa(await (await loadEmoji(getIconCode(text), "twemoji")).text()) ); } const [normal, bold] = await Promise.all([ loadFont({ family: code, text, weight: 400 }), loadFont({ family: code, text, weight: 700 }), ]); return [ { data: normal, name: `satori_${code}_fallback_${text}`, weight: 400, }, { data: bold, name: `satori_${code}_fallback_bold_${text}`, weight: 700, }, ]; }, debug: true, } ); return svgContent; } const U200D = String.fromCharCode(8205); // zero-width joiner const UFE0Fg = /\uFE0F/g; // variation selector regex export function getIconCode(char: string) { return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, "") : char); } function toCodePoint(unicodeSurrogates: string) { let r: string[] = [], c = 0, p = 0, i = 0; while (i < unicodeSurrogates.length) { c = unicodeSurrogates.charCodeAt(i++); if (p) { r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16)); p = 0; } else if (55296 <= c && c <= 56319) { p = c; } else { r.push(c.toString(16)); } } return r.join("-"); } const apis = { twemoji: (code: string) => "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/" + code.toLowerCase() + ".svg", openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/", blobmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/", noto: "https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/", fluent: (code: string) => "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" + code.toLowerCase() + "_color.svg", fluentFlat: (code: string) => "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" + code.toLowerCase() + "_flat.svg", }; export type EmojiType = keyof typeof apis; export function loadEmoji(code: string, type: EmojiType) { // https://github.com/svgmoji/svgmoji const api = apis[type] ?? apis.twemoji; if (typeof api === "function") { return fetch(api(code)); } return fetch(`${api}${code.toUpperCase()}.svg`); }