Skip to content

Latest commit

 

History

History
388 lines (291 loc) · 11.3 KB

File metadata and controls

388 lines (291 loc) · 11.3 KB

Loopwind SDK

Environment-agnostic rendering SDK for Loopwind templates. Works in browsers, Cloudflare Workers, and Node.js.

Import: loopwind/sdk

Quick Start

Browser (SVG preview)

import { renderToSvg, initSatori, loadDefaultFonts, tw, transformClassNames } from 'loopwind/sdk';
import satori from 'satori'; // or from esm.sh CDN

initSatori(satori);
const fonts = await loadDefaultFonts();

const svg = await renderToSvg({
  component: MyTemplate,
  props: { title: 'Hello' },
  width: 1200,
  height: 630,
  fonts,
});

Cloudflare Worker

import cfSatori from '@cf-wasm/satori';
import { renderToSvg, initSatori, loadDefaultFonts } from 'loopwind/sdk';

initSatori(cfSatori);
const fonts = await loadDefaultFonts();

export default {
  async fetch(request) {
    const svg = await renderToSvg({
      component: MyTemplate,
      props: { title: 'Hello' },
      width: 1200,
      height: 630,
      fonts,
    });
    return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml' } });
  },
};

Node.js

import satori from 'satori';
import { renderToSvg, initSatori, loadDefaultFonts } from 'loopwind/sdk';

initSatori(satori);
const fonts = await loadDefaultFonts();
const svg = await renderToSvg({ component: MyTemplate, props: { title: 'Hello' }, width: 1200, height: 630, fonts });

API Reference

Initialization

initSatori(satoriFn)

Initialize the Satori rendering engine. Must be called before any render.

// Browser — load from CDN
const { default: satori } = await import('https://esm.sh/[email protected]');
initSatori(satori);

// Cloudflare Workers — use @cf-wasm/satori
import cfSatori from '@cf-wasm/satori';
initSatori(cfSatori);

// Node.js — direct import
import satori from 'satori';
initSatori(satori);

initResvgWasm(wasmSource)

Initialize the resvg WASM module for PNG rendering. Only needed if using renderToPng().

// From URL
await initResvgWasm('https://cdn.example.com/resvg.wasm');

// From ArrayBuffer
await initResvgWasm(wasmBuffer);

Rendering

renderToSvg(options): Promise<string>

Render a template component to SVG. This is the core rendering function.

Options:

Option Type Required Description
component TemplateComponent Yes Template function (default export from a template file)
props Record<string, any> No User-provided template props
width number Yes Canvas width in pixels
height number Yes Canvas height in pixels
fonts SdkFont[] Yes Fonts for text rendering
config DesignConfig No Design tokens (colors, fonts, etc.). Defaults to shadcn zinc theme
animation object No Animation context for video templates
templates Record<string, EmbeddableTemplate> No Templates available for template() helper
qrGenerator function No Custom QR code generator
imageLoader function No Custom image loader (default: fetch → base64)
loadEmoji function No Custom emoji loader
debug boolean No Show Satori bounding boxes
embedFont boolean No Embed fonts in SVG (default: true)

Animation context:

animation: {
  frame: 15,          // Current frame number
  progress: 0.5,      // 0.0 to 1.0
  totalFrames: 90,    // Total frames in the video
  durationMs: 3000,   // Total duration in milliseconds
}

Example with all options:

const svg = await renderToSvg({
  component: MyTemplate,
  props: { title: 'Hello', logo: 'https://example.com/logo.png', url: 'https://example.com' },
  width: 1200,
  height: 630,
  fonts,
  config: {
    colors: { primary: '#6366f1', background: '#0f172a', foreground: '#f8fafc' },
  },
  animation: { frame: 0, progress: 0, totalFrames: 180, durationMs: 3000 },
  templates: {
    badge: { component: BadgeTemplate, meta: { size: { width: 200, height: 50 } } },
  },
});

renderToPng(options): Promise<PngResult>

Render a template to PNG. Extends RenderToSvgOptions with:

Option Type Default Description
scale number 2 Scale multiplier for high-res output
resvgWasm ArrayBuffer WASM binary (auto-inits if provided)
const { data, width, height } = await renderToPng({
  component: MyTemplate,
  props: { title: 'Hello' },
  width: 1200, height: 630,
  fonts, scale: 2,
});
// data is Uint8Array of PNG bytes

Element Processing

transformClassNames(element, twFn)

Recursively walks a React element tree and converts className props to inline style via the provided tw() function. This is what allows templates to use className="flex items-center bg-background" instead of inline styles.

import { transformClassNames, tw, DEFAULT_CONFIG } from 'loopwind/sdk';

const twFn = (classes) => tw(classes, null, DEFAULT_CONFIG, animationContext);
const styledElement = transformClassNames(rawElement, twFn);

Tailwind

tw(classes, tailwindConfig?, loopwindConfig?, animationContext?)

Convert Tailwind class strings to inline React CSS properties. Supports:

  • Layout: flex, grid, absolute, relative
  • Spacing: p-4, m-6, gap-8, px-16
  • Typography: text-6xl, font-bold
  • Colors: bg-primary, text-muted-foreground, bg-primary/50
  • Borders: border, rounded-xl
  • Animations: enter-fade-in-up/0/500, loop-spin/1000, exit-fade-out/2500/500
  • Easing: ease-out, ease-spring/1/100/10
const style = tw('flex items-center p-4 bg-background text-foreground');
// → { display: 'flex', alignItems: 'center', padding: 16, backgroundColor: '#ffffff', color: '#09090b' }

// With animation context (for video templates)
const style = tw('enter-fade-in-up/0/500 ease-out', null, config, {
  progress: 0.1, frame: 9, totalFrames: 90, durationMs: 3000,
});
// → { opacity: 0.71, transform: 'translateY(8.5px)' }

Fonts

loadDefaultFonts(): Promise<SdkFont[]>

Load Inter Regular (400) and Bold (700) from jsDelivr CDN. Results are cached.

loadFont(url, options): Promise<SdkFont>

Load a single font from URL.

const font = await loadFont('https://example.com/MyFont.woff', {
  name: 'MyFont',
  weight: 400,
  style: 'normal',
});

loadFonts(definitions): Promise<SdkFont[]>

Load multiple fonts in parallel.

const fonts = await loadFonts([
  { url: 'https://example.com/Regular.woff', name: 'MyFont', weight: 400 },
  { url: 'https://example.com/Bold.woff', name: 'MyFont', weight: 700 },
]);

Helpers

generateQr(text, options?, generator?)

Generate a QR code data URI. Uses the qrcode npm package if installed, or accepts a custom generator.

const dataUri = await generateQr('https://example.com', { width: 200, margin: 1 });

fetchImageAsDataUri(url)

Fetch an image from a URL and return as a base64 data URI.

const dataUri = await fetchImageAsDataUri('https://example.com/photo.jpg');

prepareAssets(Component, props, fullProps, options?)

Two-pass asset preparation. Renders the component once with stub helpers to discover which images/QR codes are needed, then pre-fetches them all in parallel.

Returns cached qrHelper and imageHelper functions for the real render pass. This is used internally by renderToSvg() — you typically don't call it directly.

createTemplateHelper(templates, buildProps)

Create a template() helper function for embedding templates within templates.

const templateHelper = createTemplateHelper(
  new Map([['badge', { component: Badge, meta: { size: { width: 200, height: 50 } } }]]),
  (tmpl, props) => ({ ...props, tw: myTwFn, config: myConfig }),
);

Constants

DEFAULT_CONFIG

Default design config (shadcn/ui zinc theme):

{
  colors: {
    primary: '#18181b',
    'primary-foreground': '#fafafa',
    background: '#ffffff',
    foreground: '#09090b',
    muted: '#f4f4f5',
    'muted-foreground': '#71717a',
    card: '#ffffff',
    border: '#e4e4e7',
    // ...
  },
  fonts: { sans: ['Noto Sans', 'system-ui', 'sans-serif'] },
  tokens: { borderRadius: { sm: '0.25rem', md: '0.375rem', lg: '0.5rem', xl: '0.75rem' } },
}

Types

interface SdkFont {
  name: string;
  data: ArrayBuffer;
  weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
  style?: 'normal' | 'italic';
}

type TemplateComponent = (props: Record<string, any>) => any;

interface PngResult {
  data: Uint8Array;
  width: number;
  height: number;
}

interface EmbeddableTemplate {
  component: TemplateComponent;
  meta: {
    size: { width: number; height: number };
    video?: { fps: number; duration: number };
  };
}

interface QrOptions {
  width?: number;
  margin?: number;
  errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
  color?: { dark?: string; light?: string };
}

Template Props

Templates receive these injected props alongside user-provided props:

Prop Type Description
frame number Current frame (video only)
progress number 0.0–1.0 progress (video only)
config DesignConfig Design tokens
tw (classes) => CSSProperties Tailwind class converter
qr (text) => string QR code generator (returns data URI)
image (urlOrKey) => string Image loader (returns data URI)
template (name, props?) => JSX Embed another template
path object Path animation helpers (followLine, followCircle, etc.)
textPath object Text-on-path helpers (onPath, onCircle, etc.)

Animation Classes

Templates use Tailwind-style animation classes via className:

// Entrance animations
<h1 className="enter-fade-in/0/500">         // fade in from 0ms, 500ms duration
<h1 className="enter-fade-in-up/0/500">      // fade in + slide up
<h1 className="ease-out enter-fade-in/0/500"> // with easing

// Exit animations
<p className="exit-fade-out/2500/500">        // fade out starting at 2500ms

// Loop animations
<div className="loop-spin/1000">              // spin every 1000ms
<div className="loop-fade/500">               // pulse opacity every 500ms

// Utility-based animations
<div className="enter-translate-x-5/0/800">   // slide in from x offset
<div className="loop-rotate-45/2000">         // oscillate rotation

Satori Patch

The SDK automatically patches Satori's SVG matrix transform precision from toFixed(2) to toFixed(6) during rendering. This prevents visible stepping/jumping in rotation and transform animations between frames. The patch is applied and reverted around each renderToSvg() call — no global side effects.

Architecture

Template (JSX + className) → transformClassNames(tw()) → Satori → SVG
                                                          ↓
                                                    resvg WASM → PNG

The SDK has zero Node.js dependencies. No fs, path, esbuild, sharp, crypto, or os imports. All I/O uses fetch() and standard Web APIs.

Files

File Purpose
src/sdk/index.ts Public API — all exports
src/sdk/render.ts renderToSvg(), renderToPng(), initSatori(), transformClassNames()
src/sdk/helpers.ts QR, image, template embedding helpers
src/sdk/fonts.ts Font loading via CDN
src/sdk/types.ts TypeScript type definitions