算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
figma-click-flow【设计与创意】Draws a click-flow overlay on a section of laid-out Figma screens for static handoff: a dot on each clickable element, an elbow line with rounded corners and an open arrow at the destination screen. figma.createConnector is not a function in Figma Design files, so each connector is emulated with one SVG import (hardened recipe in references/draw-over…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Draws a printed user-flow overlay on a section of laid-out screens. Each connection: a red dot at the click target on screen A, an elbow line with rounded corners ending in an open arrow at screen B's nearest edge. Reference look: a flow section of one production file.
REQUIRED PREREQUISITE: load figma-canon + pass figma-preflight before any figma_execute write (the write path is the figma-console MCP server through its Desktop Bridge plugin). With only the official Figma MCP server connected, the write goes through use_figma on path B of figma-preflight Check 0 (not validated by this repo: keep writes small, read back every change), and Phase 4 validates with get_screenshot within the budget in figma-canon/references/rate-limit-recovery.md.
figma.createConnector throws not a function in Figma Design files (verified via figma_execute, field note 2026-05). CONNECTOR nodes that already exist in design files were created via the UI (Shift+L shortcut) and cannot be cloned: node.clone() errors with Cloning CONNECTOR nodes is not supported in the current editor. This skill therefore emulates the connector look by importing one SVG per connection through figma.createNodeFromSvg.
reactions on nodes) and wants the same connections visible in static handoff.(from-element-id -> to-screen-id) pairs.figma.createConnector directly.figma_execute after figma-preflight.Verified against a reference node in one production file. refConn in the right column is the reference connector.
| Property | Value | Source of truth |
|---|---|---|
| Stroke color | #E5484D | Neutral default, configurable. The reference used one dominant red in 20+ instances; that value was project-specific |
| Stroke weight | 4 px | refConn.strokeWeight === 4 |
| Path | Elbow with rounded corners, R = 12 px | connectorLineType: 'ELBOWED' + visual radius pixel-picked |
| Origin marker | Filled circle, r = 5 px (10 px diameter) | connectorStartStrokeCap: 'CIRCLE_FILLED' |
| End cap | Open arrow, two strokes back from tip at ±28°, length 12 px | connectorEndStrokeCap: 'ARROW_LINES' |
| Layer name | flow-overlay · click-connectors (kebab-case with a middle-dot separator, from one project's own layer canon; keep this exact name, because the rerun and the cleanup snippet find the overlay by it) | figma-canon/references/naming-canon.md |
The stroke color #E5484D is a neutral default and is configurable: override it through the strokeColor input (STROKE_COLOR in the recipe) when the file has a brand-specific accent.
sectionId: "123:456" # required
mode: from-prototype # default; OR "explicit" OR "auto-layout"
pairs: # used when mode === "explicit"
- from: "123:789" # element that gets tapped
to: "123:790" # destination screen
triggerFilter: ["ON_CLICK", "ON_PRESS"] # default; ignores hover/drag
includeOverlayActions: true # default; sheets count as navigation
strokeColor: "#E5484D" # default; see Visual canon
cornerRadius: 12 # default
strokeWeight: 4 # default
originAnchor: "auto" # default; "center" | "edge-toward-dest" | "auto"
Each input is a constant in the CONFIG block of references/draw-overlay.js: sectionId is SECTION_ID, mode is MODE, pairs is EXPLICIT_PAIRS, triggerFilter is TRIGGER_FILTER, includeOverlayActions is INCLUDE_OVERLAY_ACTIONS, strokeColor is STROKE_COLOR, cornerRadius is CORNER_RADIUS, strokeWeight is STROKE_WEIGHT, originAnchor is ORIGIN_ANCHOR. The recipe also has DOT_RADIUS, ARROW_LEN, AUTO_LAYOUT_MIN_AR and AUTO_LAYOUT_ROW_GAP, which have no input above.
mode reference:
from-prototype (default): walks reactions[] on every descendant. Best when designer wired the prototype.explicit: uses pairs. Best when the caller has a known list (from a PRD, a spec that lists screen transitions, or a hand-curated list).auto-layout: groups top-level FRAMEs in the section into rows by Y, sorts each row by X, builds intra-row left-to-right pairs. Best when prototype isn't wired and screens are sequentially laid out (like a numbered catalog). Heuristic: verify visually before trusting; rows mixing scenarios may produce wrong connectors.
AUTO_LAYOUT_MIN_AR). This filters out labels and dividers. For desktop or landscape screens, lower that constant, or the run returns drawn: 0.AUTO_LAYOUT_ROW_GAP) below the previous one starts a new row.originAnchor reference:
auto (default): bbox center for small nodes (<200×200 = buttons, list rows), edge-toward-dest for screen-sized nodes (any node at least 200 px in both dimensions). Right behavior for both prototype-driven mode and screen-level adjacency.center: always bbox center. Use when prototype reactions are wired on small interactive elements and you want the dot ON the button.edge-toward-dest: nearest-edge midpoint. Use for screen-to-screen adjacency where center would put the dot deep inside the screen body.If user just says "wire it up" with no detail, default to mode: from-prototype.
Walk up to the page ancestor (sections may be nested inside sections), call figma.loadAllPagesAsync(), then await figma.setCurrentPageAsync(pageNode). Use figma.getNodeByIdAsync exclusively: sync getNodeById errors under documentAccess: dynamic-page.
Server note for use_figma (official Figma MCP server): Figma's figma-use skill lists figma.loadAllPagesAsync() as not implemented in that runtime, and says a read of a member the node type does not have throws TypeError: node.X: no such property 'X' on Y node, optional chaining included (figma/mcp-server-guide, read 2026-10). So the recipe calls figma.loadAllPagesAsync() inside try/catch and carries on without it (setCurrentPageAsync loads the page anyway), and it checks "reactions" in n before reading reactions, because SECTION, SLICE and COMPONENT_SET nodes do not declare it. In figma_execute both guards change nothing. This repo has not run the recipe on use_figma.
Walk descendants of the section. For each node, inspect reactions[], and in each reaction read actions[] (the recipe falls back to the singular action field when actions is absent). Collect a pair when:
action.type === 'NODE' (navigate to frame), ORaction.type === 'OVERLAY' (open sheet/modal, counts as flow), ANDreaction.trigger.type is in the triggerFilter (default: ON_CLICK, ON_PRESS, ON_TAP).The recipe keeps a reaction that has no trigger.
Drop pairs where from or to cannot be resolved or lies outside the section's bounding box (likely external/cross-page links). Dropped pairs come back in the dropped array of the return value with a reason (node not found or outside section): report them instead of losing them silently.
If 0 pairs found, return early with a warning so the caller can switch to mode: explicit and supply pairs manually.
For each pair compute (all in absolute page space):
origin = anchor on from node's absoluteBoundingBox, picked by originAnchor: bbox center for small interactive nodes, nearest-edge-toward-dest for screen-sized FRAMEs (auto default), or as overridden.dest = nearest-edge midpoint of to frame's absoluteBoundingBox (left / right / top / bottom).(midX, origin.y) then (midX, dest.y) where midX = (origin.x + dest.x) / 2. Same right-angle topology as connectorLineType: 'ELBOWED'.cornerRadius (default 12), inserted as Q-curves.If |origin.y - dest.y| < 2 * cornerRadius, drop the elbow and draw a straight line (avoids degenerate corners).
Per pair, build an SVG string with:
<circle> at origin (origin marker).<path> with the elbow + Q-corners (the trunk).<polyline> arrowhead at dest.Import via figma.createNodeFromSvg(svgString), which returns a FRAME with vector children. Position the imported frame at (minX, minY) (the SVG's top-left in page space).
Coord-space rule (verified): SECTION children use section-local coords. After section.appendChild(overlay), set overlay.x = 0; overlay.y = 0 so overlay aligns with section's top-left. Then position each imported SVG frame as a child of overlay using overlay-local coords (subtract section absolute origin).
vectorPaths.windingRule: 'NONE' is accepted by the API (verified). Auto-fit moves a vector to its path's leftmost coord after vectorPaths is set; trust this rather than fighting it.
See references/draw-overlay.js for the complete hardened snippet. The same snippet is written to run in use_figma of the official Figma MCP server too, with the guards described in Phase 0 (not validated on that server by this repo); the timeout figures below are for figma_execute only.
figma_execute has a default timeout of 5000 ms and accepts up to 30000 ms. After a timeout or an error, do not rerun at once: this recipe removes the prior overlay and then draws a new one, so first read figma-canon/references/figma-execute-atomicity.md and the section "figma_execute timeout on bulk clone and font loads" in figma-canon/references/plugin-api-anomalies.md.
Use figma_capture_screenshot (plugin export). REST-backed screenshots return HTTP 429 (rate limited) after a handful of calls in a batch; see figma-canon/references/rate-limit-recovery.md. Do not use figma_take_screenshot for large sections.
As of figma-console-mcp v1.40.8, figma_take_screenshot uses the Desktop Bridge plugin when connected and falls back to the REST API, while figma_capture_screenshot always uses the plugin runtime and needs the bridge. The rule above still holds: figma_capture_screenshot is the call that always stays on the plugin runtime.
Check:
Idempotent rerun: skill auto-removes any prior flow-overlay · click-connectors frame inside the section before drawing. To remove without redrawing (here section is the section node, from await figma.getNodeByIdAsync(sectionId)):
const overlay = section.findOne(n => n.name === 'flow-overlay · click-connectors');
if (overlay) overlay.remove();
| Mistake | Fix |
|---|---|
figma.createConnector(...) | Not a function in design files; emulate via SVG import |
node.clone() on existing CONNECTOR | Throws "Cloning CONNECTOR nodes is not supported" |
figma.getNodeById | Use getNodeByIdAsync; sync version errors under dynamic-page |
Set overlay.x = section.x before append | Section children use LOCAL coords, produces double offset; set x=0; y=0 AFTER append |
| Validate with a REST-backed screenshot | figma_take_screenshot returned 429 on the large sections of one production file; use figma_capture_screenshot |
Filter only action.type === 'NODE' | Misses OVERLAY (sheet open), common in mobile flows; include both |
Ignore reaction.trigger.type | Hover/drag triggers pollute the overlay; filter to ON_CLICK/ON_PRESS/ON_TAP |
| Draw nothing when prototype unwired | Detect 0 pairs and return early with a warning, not a silent empty overlay |
| Sharp 90° corners | Use Q-curves with R=12 to match reference rounded look |
Passing section.parent to figma.setCurrentPageAsync | Walk up parent chain until type === 'PAGE'; nested sections break naive section.parent |
| Need | Call |
|---|---|
| Discover prototype reactions | node.reactions[].action.{type:'NODE'|'OVERLAY', destinationId} (the recipe reads actions[] and falls back to action) |
| Filter by trigger | ['ON_CLICK','ON_PRESS','ON_TAP'].includes(reaction.trigger?.type) |
| Build connector SVG | <circle> + <path> (elbow w/ Q-corners) + <polyline> arrow |
| Import SVG | figma.createNodeFromSvg(svgString) returns a FRAME with VECTOR children |
| Default red | #E5484D (rgb 0.898, 0.282, 0.302) |
| Cleanup | section.findOne(n => n.name === 'flow-overlay · click-connectors')?.remove() |
figma-canon + figma-preflight: required before any figma_execute write.figma-orient: only run if file is unfamiliar this session; skip otherwise.references/draw-overlay.js: full hardened snippet, copy + parameterize for use. It is a figma_execute body (top-level await and return), not a standalone Node script.figma-canon/references/figma-execute-atomicity.md: what to do after a figma_execute timeout or error before retrying.