Bring a design or static assets into a scene as LAYERS, not flat frames, whenever they are going to move — Figma frames for an ad or reel, or a Figma design, logo, UI screen or screenshot that animates inside a tutorial, promo or explainer — each layer in its native format (live text, vector or raster) on a fetchable hosted URL, with flat frames only as a fallback. Stage 1 of the `animate-design` skill's pipeline, and used before the motion-design-craft skill by any flow that animates a design.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Design Layer Import?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/picsart-design-layer-import)
---
name: design-layer-import
description: Bring a design or static assets into a scene as LAYERS, not flat frames, whenever they are going to move — Figma frames for an ad or reel, or a Figma design, logo, UI screen or screenshot that animates inside a tutorial, promo or explainer — each layer in its native format (live text, vector or raster) on a fetchable hosted URL, with flat frames only as a fallback. Stage 1 of the `animate-design` skill's pipeline, and used before the motion-design-craft skill by any flow that animates a design.
metadata:
owner: motion-designer
user-invocable: false
---
# Stage 1 — Import: the design in as layers
Motion design animates *elements*, so the import's whole job is to bring the design in as
**layers, not flat frames**. A frame delivered as one sealed PNG can only ever
cross-dissolve; a frame delivered as its layers — panels, headline, buttons, the image,
the list — is a thing whose elements can slide, scroll, type on, parallax and
smart-animate. **Everything downstream is gated on this**: if the layers are lost here,
no amount of tuning later can recover the ad; it stays a slideshow.
## Pull the layer tree, per frame
Before reading Figma, post the one-line progress message the `animate-design` skill describes
("Reading your layers…").
**With the Figma connector present**, read structure before pixels: `get_metadata` /
`get_design_context` to walk each frame's node tree, then export each animatable node as
its own asset. For every layer capture:
> **AUDIT EVERY NESTED LAYER — never compose from Figma's simplified, frame-level output.** The
> single biggest way items go missing from the ad: Figma's frame-level / summarized read (a shallow
> `get_design_context`, or a `get_metadata` that stops at the top level) **collapses nested children**
> — groups, component instances, auto-layout children, masked contents — into a summary. Take that at
> face value and **every nested element is silently dropped**, so the composed frame is missing pieces
> that were plainly in the design. **Recursively expand each frame's FULL tree** (`get_metadata` on the
> frame node with its children expanded, or `use_figma` to walk *all* descendants) and audit it
> **node by node** — do not trust a frame-level layer list to be complete. **Completeness check:** the
> layers you capture must match the count of leaf/animatable nodes in the *fully-expanded* tree, not
> the handful the frame-level view showed. If a group or instance shows as one node, open it — its
> children are the layers. (Downstream this is the Stage-2 "layer count ≈ node count" gate; catch it
> here, at the read, not after a frame composes short.)
- **id** and **name/role** (what it is — `chrome`, `headline`, `cta`, `image`, `list`, `fx`)
- **type** — `text`, `vector` (icon/logo/shape), or `raster` (photo/complex image); plus `group`
- **transform** — `x, y, w, h, rotation, opacity`, and **z-order** (front-to-back)
- **parent/group** — so a group can move as one: record the group (card, control, grid cell) each
layer belongs to, and which member is its backing. Compose turns it into one group that carries
the motion (motion-design-craft skill, `references/AUTHORING-FLOW.md` §6); without it the members are
keyframed one by one and drift apart
- **for an image fill, its `scaleMode`** (`FILL` / `FIT` / `CROP` / `TILE`), and for `CROP` its
`imageTransform` — they decide the layer's `fit` and `crop`; a raw image placed without them is
stretched to the box
- **for a text layer, also the type properties** — the **string**, resolved **font URL** + size,
**horizontal alignment** (read Figma's `textAlignHorizontal` — `left`/`center`/`right`; *never*
guess it from position or from "it's a big headline"), and the **box sizing/wrap**: a Figma
**fixed-width** text box that runs past the frame **clips, it does not wrap** — record the box
width and that it clips, or the renderer soft-wraps the line into two downstream.
### Export each layer *as what it is* — PNG is the fallback, not the default
The layer's **source type** decides the export format. Deciding this per layer is the job;
reaching for PNG on every element is the freelance-default that quietly wrecks quality —
**stop and ask "is this layer actually raster?" before you export a PNG.**
- **Text** → keep as **live text** (the string + resolved font URL), *never* an image. This is
what makes type-on, per-line and per-character animation possible; baking text to PNG kills
all of it and can't reflow.
- **Vector** — icons, logos, line art, panels, pills, flat shapes → a **shape layer**, no asset
at all, built in two steps. **First create the layer**: `picsart_scene_patch` adds a
layer whose `content.kind` is `"shape"`, with a `rect` (or `ellipse`) item sized to the node's
box and the node's placement in the layer's `transform`; for a non-rectangular vector (an icon,
a logo, a boolean), add the shape layer and append its outline with
`picsart_scene_import_svg_path` (the node's path data as `d`). **Then paint it**:
`picsart_scene_import_figma_paint` takes the `scene` and that layer's `layerId`, plus the
node's CSS from `get_design_context` — `fill`, `boxShadow`, `gradient` (with the element
`size`), and `gradientPaint: "stroke"` (+ `gradientStrokeWidth`) for a gradient outline. A
solid stroke is not a paint it takes: it comes in with a path outline (`import_svg_path`'s
`stroke` + `width`), or, on a `rect`/`ellipse` layer, as a stroke item added with
`picsart_scene_patch`.
`import_svg_path` reads every SVG path command, absolute and relative (M L H V C S Q T A Z,
repeated coordinate sets included): quadratics are elevated to cubics, arcs decomposed into
cubic beziers, and the smooth shorthands reflect the previous control point. Each subpath
becomes one `path` item spelled the Lottie/AE way (`{ closed, vertices: [{ point, inTangent,
outTangent }] }`, tangents relative to their point, content-local px); a closed subpath folds its
closing tangent onto the start vertex. A gradient is a top-level paint and cannot reach paths
nested inside the import's paint group, which is why a gradient outline is imported geometry-only
onto its own shape layer and painted after.
A shape layer stays crisp at any scale and keeps every motion a shape has — radius and size
tweens, recolour, draw-on — where an SVG image only moves as a whole. Whatever the paint tool
lists in `skipped` (an inset shadow) is the one part to bring in another way; rasterize
**only** a node that is not a shape.
- **Raster / photographic** — the hero image, a photo, a gradient/blur that won't survive as
vector → **PNG with transparency** so the layer floats over what's behind it. Take the image
fill's original bitmap from `download_assets` → `rawImages` (the largest), **never the node
`export` or `get_screenshot`**: both draw the node over its frame's background colour, so a
cutout photo on a dark frame comes back on an opaque black box. The raw bitmap is framed as
uploaded, not as the layer shows it: place it by the fill's `scaleMode` (`FILL` → `fit:
"cover"`, `FIT` → `"contain"`, `CROP` → `"cover"` plus `crop = [tx·W, ty·H, a·W, d·H]` from its `imageTransform`
`[[a, 0, tx], [0, d, ty]]` on a W×H bitmap).
If you're about to export a PNG of an icon, logo, or shape, that's the tell you *defaulted*
instead of decided — go back and take it as vector.
**Settle output scale in the first five minutes, not the fiftieth.** Whether the ad ships at
one size or several (multiple ratios/resolutions, or any later up-scale) is what makes vector
the *normal* case, not an edge case — a PNG baked at one scale is soft or jagged at any other,
while vector is resolution-free. Decide this up front (see *Set the format* below); don't
discover the constraint at export time and re-import. And when you do capture
a vector layer, **declare it `type: "vector"`** — silently recording a shape as `type: "image"`
is the same defaulting mistake wearing a different hat.
**Format doesn't excuse hosting:** a shape layer carries no asset, but every raster you export
still has to be lifted onto a **Picsart-hosted `https://` URL** (below). A `figma.com` URL is
CSP-blocked in the preview — export → upload → author the returned Picsart URL.
### Granularity — split to what moves
Not every node, and not one blob. **A layer is anything that either moves independently or
persists across frames.** Group the rest.
- Split out: the headline, each CTA, the hero image, a scrollable list, a tap target,
anything that will animate on its own.
- Keep grouped: static decoration, a card's fixed internals, backgrounds.
- Over-splitting (hundreds of nodes) is noise the designer must wrangle; under-splitting
(the whole screen as one image) is the slideshow trap in disguise. When unsure, split
by what the reference ad would animate.
### Import every element the design shows — visibility is the design's call, not yours
Import **every element the design renders**. A layer is dropped for exactly one reason: the
**design itself hides it** (Figma `visible:false` / a `hidden` node) — that is the designer's
signal, and even then a *prominent* hidden layer is worth a note, since it may be meant to appear
mid-animation. **Never drop a layer because *you* judged it "won't be seen":**
- **Partially on-frame** (clipped by the frame edge) is **on screen** — it shows in the very first
hold, so omitting it is a visible hole against the design. This is the frame-6 tell: the search
pill and the tag row are *clipped, not absent*, and dropping them leaves the composed frame
missing content the mockup plainly shows.
- **Fully off-frame is not a licence to drop it either.** In motion a wide row or an off-edge
element is usually there to **scroll or pan into view** — import it and let choreography decide,
don't pre-empt the motion by deleting it (that is how a design's intended scroll becomes a
static gap).
A judgment call to leave something out is **surfaced to the designer, never silent** — same rule
as a missing font. The Stage-2 `get_screenshot` compare is what catches a dropped element, as a
hole where the design has content.
## Tag matched elements across frames
The thing that makes an interface *move* between screens instead of dissolving is a
**matched element** — a layer present in consecutive frames that should animate from its
state in A to its state in B. So as you import, give any recurring layer a stable
**`matchKey`** (the same key on the header in every frame it appears in). Match by
name/role first, geometry second. `motion.json.matched[]` is built from these keys, and
Stage 2 keeps each matched element as **one continuous layer**, not a fresh copy per
frame. Miss this and the compose step can only cross-dissolve — the biggest "slideshow"
tell there is.
**A recurring element keeps its `matchKey` even when it is transformed or its content changed** —
a title that moves and shrinks, a headline whose wording grows (`AI` → `AI Effects`), a button that
recolours. Match by **semantic role** ("the title", "the CTA"), *never* by exact string or position:
`AI` and `AI Effects` are the same title. The fact that the same element is **present in both frames
but different** is the *strongest* match signal there is — it means the element should **animate from
A to B**, and it is precisely the case where matching must not fail. A same-role text or object that
is merely repositioned/resized/reworded must **never** be tagged as one element leaving and a
different one arriving; that misread is what makes it disappear-and-reappear (or get papered over
with a transition) downstream instead of moving.
**Matching is at the layer level — so an assembling sentence needs the layers split to match.**
The rule "a header in frames 1–4 is one layer with four transform states" is right for chrome, but
it breaks for text that *grows*: frames whose text reads `AI` → `AI Effects` → `AI Effects made
easy.` are usually **one whole-string text node each**, i.e. three differently-named layers — so
nothing marks `AI` as persisting; the thing that actually persists is a **substring**. Capture it
that way: split the shared words into their **own layer** carried by one `matchKey` across the
frames, and each added clause into its own layer that `enter`s. A single whole-string node per
frame gives the diff three different strings and no persisting layer, and the assembling effect
collapses to a cut. (This is the import half of `MOTION-PATTERNS.md` *"Progressive text build."*)
The A→B **delta** itself (how far a matched layer moved, what actually changed) is not judged
by eye at motion time — it is **read with `picsart_scene_diff_layouts`** (which resolves both
frames through `query_layout` and returns the classified delta; `query_layout`-on-both is the
fallback when the tool is unavailable). The `matchKey` you set here is exactly what that diff
takes as its `matchHints` to pair the layers up — a dropped key becomes a missed match there.
The full frame-diff procedure is in
`references/MOTION-PRINCIPLES.md` in the motion-design-craft skill §1.
## Get every asset onto a real URL
A Figma export and a `file://` path are both **unfetchable by the renderer**. So each
layer asset that is not already an `https://` URL must be lifted onto one: `picsart_drive_upload_ask`
(the drag-and-drop panel) or `picsart_drive_upload`, then **read back the resulting `https://` URL and
author *that*.** Text and fills carry no asset and need no lift.
**A scene asset's `uri` must be a Picsart-hosted `https://` URL (`pastatic`/`gcdn`, from
`picsart_drive_upload_ask` / `picsart_drive_upload`) — never a base64 `data:` URI, and never a `figma.com`
URL.** The interactive **preview engine runs in a browser with a Content-Security-Policy** whose
`connect-src` allowlist excludes both — they get **blocked → blank** in the editor preview
(`csp: blocked connect-src … figma.com/….svg`), even though the scene *validates* and the
server-side `export`/`contact_sheet` render them fine. That server/browser split is the trap: a
`figma.com` URL or a `data:` URI is only ever an **intermediate** — hand it to `upload`/`drive` and
author the **Picsart-hosted URL it returns**. **`validate_scene` and a clean `contact_sheet` do NOT
prove it renders in the preview** (both are lenient server-side paths) — the only reliable proof is
the editor preview itself, so default to Picsart-hosted URLs and it's fine everywhere.
**Drive full (`failure_space_limit_reached`) is status, not a retry.** Every file in the batch fails
the same way, so stop at the first one — don't send the rest or the next batch. Tell the designer
their Drive is full and how many assets still need a home, then:
- **Shrink the upload first.** Shapes, text and flat fills need no asset; only true rasters do.
A batch full of icons and panels is the vector rule above being skipped.
- **Offer cleanup, never do it blind.** `picsart_drive_list` (`flat: true`) shows what is there;
move only the files the designer names to trash (`picsart_drive_delete` without `permanent` — recoverable),
after their explicit yes.
- **Never route around it** with a `blob:`, `data:` or `figma.com` URL: that is the blank preview
(`csp: blocked img-src … blob:`), just later.
## When layers cannot be had — the honest fallback
Two cases force flat-frame ingest, and both are a **degrade, announced**:
- **No Figma connector.** Suggest it once, and offer the fallback in the same turn —
never block, never re-ask, and you cannot connect it for them:
> Connecting the Figma connector (Settings → Connectors → Figma) lets me pull your
> design's layers, which is what makes real motion design possible. Without it I can
> only animate whole flat frames — a slideshow. Share the Figma file, or paste flat
> frame images to do the simpler version.
- **Figma export quota'd out.** Figma export is metered **per Figma seat** and can quota
out mid-batch — that is **status, not an error**: continue on the upload path for the
remaining assets, and if only flats are reachable, say the result will be a slideshow
and offer to resume layered when the seat's quota returns.
A flat frame is imported as a single `image` layer filling the composition — the montage
fallback in the `animate-design` skill. Say plainly what is lost: *"without the layers this is a
slideshow, not motion design."*
## Order — take Figma's, never ask for it
The running order is **Figma's own** — the frame/artboard sequence in the file (or, for
pasted images, the order they were given in). **Take it as authoritative, record it, and
move on — do not ask the designer to confirm it.** A question the source already answers is
noise — the house rule: *"don't turn something the pipeline
already knows into a user choice."*
Reordering is **optional and designer-initiated**: it happens on the storyboard board
(shown for a glance and a drag, never a mandatory confirm) or when the designer explicitly
says "swap 2 and 3." Absent that, Figma order stands, silently. Never open an
ASK (with your host's own question tool, such as AskUserQuestion, so the options are buttons the user taps, the recommended one first; only where the host has none, the options numbered in one message; in unattended pace, never ask: write it to the project record's openDecisions with the answer you assumed, as picsart-video-rules says) to confirm the running order.
## Set the format — ask the ratio and fps
Ask the designer **once, at the start**, in one ASK with two choices, each prefilled with your
pick from the material so they confirm in one reply:
- **ratio** — one of **9:16, 16:9, 1:1, 4:3, 3:4**. Suggest the frames' own aspect (1080×1920 →
9:16 at 1080×1920).
- **fps** — one of **24, 30, 60**. Suggest 30 unless the design says otherwise.
Do not ask anything else here: not the running order (take the design's own), not a total
duration (it derives from the motion), not the energy. Read the **energy** from the design (see
below), as `calm`, `standard` or `energetic`, and state it in the same message so the designer can
overrule it. Store the answers as the project's output target (ratio → width/height, fps) and
use the default hold and seam.
Also:
- **output scale/sizes** — not part of that ASK, so default to a single size, but
**surface the question when it's genuinely open**: will this ship at one size or several (multiple ratios/resolutions, or a later up-scale)?
The answer decides raster-vs-vector for every layer above, and "multiple sizes" makes vector the
norm — cheap to settle now, expensive to discover at export. When it isn't obvious, a plain
an ASK is the right surface; otherwise state the single-size default and move on.
Once set, the designer can still adjust the format
in plain conversation ("make it square", "60fps", "more energetic") — a normal message.
**`energy` is a productive↔expressive dial**, not just "fast vs slow": *productive* =
subtle, efficient motion for routine beats; *expressive* = vibrant, highly-visible motion for
significant moments (an opening, a primary CTA, a hero reveal). It scales the motion's duration
and easing register downstream — see
`references/MOTION-PRINCIPLES.md` in the motion-design-craft skill §6.
Two things are never asked: **order** (taken from Figma silently, above) and **total duration**
(derived from the motion by default; a soft target is the designer's own pick, and
a designer who wants it tighter says "punchier").
## Output → `motion.json`
Write frames-of-layers and the target (schema in the `animate-design` skill). Each frame carries
a `layers[]` with `id`, `role`, `type`, `transform`, `asset`/`content`, and `matchKey`
where it recurs; `matched[]` is assembled from the keys.
## Gate to pass Stage 1
Print it with ✓/✗ before compose:
- Every frame imported as **layers** (or consciously flagged flat-fallback with the
reason recorded).
- Every animatable layer has an **asset at a real `https://` URL** (or live text/fill)
**and a transform** with z-order.
- **Every element the design shows is imported** — nothing dropped on a self-made "won't be
visible" call; a partially-clipped or off-frame layer is still imported. Only designer-hidden
layers (`visible:false`) are omitted, and a prominent one is noted.
- **Matched elements tagged** with `matchKey` across the frames they appear in.
- **Groups recorded** — every card/control/cell member names its group, so it moves as one.
- Each layer captured **in its native format** — text is live text, icons/logos/shapes are
`type: "vector"` (a shape layer), only genuinely raster content is PNG. No vector or text baked to
PNG by default.
- **Order** recorded, **target format** set, and **output scale/sizes** settled.
A frame that came in flat while its layers were available is a **gate failure, not a
shortcut** — the whole pipeline's value is here.
## Mistakes
- Exporting flat frames when the Figma layers were right there — the slideshow trap at
its source.
- **Silently dropping a design element because you judged it "won't be visible."** A
partially-clipped element is *on screen now*; a fully off-frame one usually *scrolls in*. Only a
designer-hidden layer is a legitimate skip — and even that is surfaced if prominent, never a
silent judgment call.
- Losing transforms/z-order, so composed layers stack or land wrong.
- Not tagging matched elements, forcing cross-dissolves later.
- Over- or under-splitting: hundreds of nodes to wrangle, or one blob that cannot move.
- **Flattening a vector or text layer to PNG by default** — text should stay live, icons/logos/
shapes stay vector; PNG is only for genuinely raster content. Reaching for PNG per element is
the freelance-default that softens the ad at any other scale.
- **Taking a raster from the node `export`** — it flattens transparency onto the frame's
background, so a cutout lands on a black box. Use the fill's `rawImages` bitmap.
- **Declaring a shape layer `type: "image"`** — recording vector as raster is the same default
wearing a different hat; it loses the resolution-free path.
- **Dropping a text layer's alignment or box behaviour** — capturing only `x/y/w/h` and then
guessing `center`, or losing the fixed-width-clip box so the line soft-wraps to two downstream.
Read `textAlignHorizontal` and the box sizing from Figma; don't infer them from position.
- **Not settling output scale up front** — discovering "it ships at three sizes" after baking
single-scale PNGs means a re-import; ask it with the ratio and fps in the first five minutes.
- Binding a Figma link or local path directly — unfetchable; lift to a Picsart-hosted URL first
(a `figma.com` SVG is CSP-blocked in preview just like any figma.com asset).
- Nagging about the Figma connector after the designer already chose the flat path.