## Figma source (optional, needs a token) Use this path only when `FIGMA_TOKEN` (or `FIGMA_ACCESS_TOKEN`) is set and the source is a Figma link with a node id. Fill `` and `` only with values matching `^[A-Za-z0-9]+$` and `^[0-9]+[:-][0-9]+$`; anything else is not a Figma link. The token is a secret: pass it only as a header from the environment, never print it, echo it, write it to a file, or put it in chat or a commit. ```bash _FT="${FIGMA_TOKEN:-$FIGMA_ACCESS_TOKEN}" printf 'X-Figma-Token: %s\n' "$_FT" | curl -fsS -H @- "https://api.figma.com/v1/files//nodes?ids=&geometry=paths" -o "/figma-nodes.json" printf 'X-Figma-Token: %s\n' "$_FT" | curl -fsS -H @- "https://api.figma.com/v1/images/?ids=&format=png&scale=2" -o "/figma-ref.json" ``` The nodes response gives the tree, bounds, fills, effects and radii; the images response gives a URL for the **reference frame**, saved as `reference.png`. Every layer position is its absolute bounds minus the frame's origin. Figma's image CDN sometimes resets connections: retry each download and check each PNG's size before using it. ### Layer export Export every element that will move as its own PNG at 3x through `/v1/images`, 20 to 25 ids per call. Placement: | Case | Do | |---|---| | PNG size equals `absoluteRenderBounds` (text always does) | Place at the render bounds | | PNG larger than the bounds, even overhang (a clipped effect) | `x = bbox.x − (pngW − bbox.w) / 2`; `y` from `renderBounds.y` unless the top was clipped, then from the effect radius and offset | | Uneven overhang (an arrowhead, one square cap) | Bounds of `strokeGeometry` or `fillGeometry` from `geometry=paths` | | Rotated vectors with large blur, arcs, donuts, partial ellipses | `use_absolute_bounds=true`, placed at the bounding box; blobs can be 1x | | Zero-width or zero-height stroked vectors | Without `use_absolute_bounds` (it returns null for them) | | Mask group with blurred content | Anchor on the edge that was not clipped | | Pattern far larger than its surface | `use_absolute_bounds=true`, then crop to the surface | | Instance children (`I12:34;56:78`) returning null | Export the parent instance | | A photo baked into a larger layer | Export the photo node too; cover the baked copy with a skeleton and put the photo above for the `photo` resolve | - Text stays an exported layer in a showcase; retyping it changes kerning and line metrics. - Keep Figma's z order, static layers included. Frame strokes usually draw above children: render a card's stroke as an overlay above its clipped content. - A container whose children move separately is rebuilt in CSS from the node data (fills, per-corner radii, inside strokes including `individualStrokeWeights`, effects), with each child exported alone; exporting the container bakes its children in. - A Figma drop shadow with blur R is `box-shadow` blur R on one element, or `filter: drop-shadow()` blur about R/2 on a group. A shadow on joined surfaces is the shadow of their union: one static container with `drop-shadow`, never per-element shadows that darken the overlaps. - The background is the original image fill (`GET /v1/files/:key/images`), with the fill's scale mode and `imageTransform` applied in CSS. - Skip hidden layers and layers buried under opaque shapes. **Drawing a line on.** Figma outlines strokes into filled shapes, so write the centreline yourself from `strokeGeometry` (halfway across the stroke; a rounded corner's centreline radius is the outline's inner radius plus half the stroke) and use it as a mask over the exported PNG: ```html ``` Animate `stroke-dashoffset` from 1 to 0 on the JS clock; pulses along the line read positions with `getPointAtLength` on the same path. Adapted from Elaya's [ai-design-skills](https://github.com/elayadesign/ai-design-skills) design-to-motion (MIT, © 2026 Elaya), rewritten for gstack. See NOTICE.md.