@frames.js/debugger
Version:
Debug your Frames.js applications
200 lines (179 loc) • 4.96 kB
text/typescript
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`);
}