SuperAPI AI Directory

design-layer-import

Design & creation

View repository

Updated 10-30 21:07

About this skill

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 enters. 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 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.