Files
figcraft/docs/interaction-measurements.md
T
Trevor BrownandClaude Sonnet 5.5 39acc36438 Docs: the interactions measured against Figma on 2026-11-08, case by case (#2111)
* Docs: the interactions measured against Figma on 2026-11-08, case by case

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at low

* Docs: the measured values for snapping, menus and Escape, and what FigCraft did before each fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

* Docs: the safe triangle unresolved, the Hug notice fixed in #2099, the variant warning not added (#2111 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

* Docs: partly fixed defined, the test row a match, open questions without repeats, ui-parity's line reworded (#2111 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 01:47:38 +01:00

190 lines
17 KiB
Markdown

# Interaction measurements
> **Last reviewed:** 2026-11-08 · **Last updated:** 2026-11-08 · **Change:** minor (first record of the interactions measured against Figma, case by case) · **Target:** Figma Design web app, measured 2026-11-08
On 2026-11-08 a set of interactions was measured in Figma's web app and compared with FigCraft's: modifier keys on move, resize and rotate, arrow keys, zoom steps, auto layout Fill and Hug, instances, components, number field scrubbing, sidebar widths, auto-pan at the viewport edge, copy and paste in vector edit mode, duplicate and paste placement, and text-editing keys. Every measurement used synthetic files, driving the UI and reading the Design and Layers panels (field values, dropdown labels, bounding boxes), or counting pixels on screen where a panel could not show the number. No incumbent screenshots are committed. This complements [ui-parity.md](ui-parity.md) and works on gap 4 in [gaps.md](gaps.md), "Interaction fidelity not compared tool by tool". It is not a full pass: each section lists what was covered, and the open questions at the end list what could not be measured.
Status values: **matches** (FigCraft already behaved as measured), **fixed in #NNNN (open)** (a mismatch found, with the pull request that fixes it, open when this was written; the pull request carries its tests and CI), **partly fixed in #NNNN (open)** (the same, with what is still missing named), **not measured** (with the reason), **inconclusive**. Error margins are given where the measurement had one. Trust the shapes of the rules more than the last digit of a number.
## Move, resize and rotate modifiers
Synthetic pointer sequences on the canvas with modifier flags set. Resize handles were pressed 4 px inside the edge, so absolute resize numbers are about 8 px short per side; the cases were read relatively.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Shift while moving | locks to the dominant axis (drags of +100,+30 and +100,+90 both gave dy 0); no 45 degree snap | the smaller of dx and dy is zeroed | matches |
| Alt while moving | a copy stays at the original place and the selection moves (layers 1 to 2) | duplicates on drag, re-synced with the Alt key | matches |
| Ctrl while moving | inconclusive: with Ctrl from the start the layer did not move, with Ctrl late in the drag it still snapped; a synthetic Ctrl does not seem to reach Figma as the modifier | Ctrl or Cmd turns snapping off | not measured (synthetic Ctrl is not delivered as a modifier) |
| Shift while resizing | corner (+50,+10) on a 100 by 50 layer keeps the ratio; right edge (+50,0) scales the height too, about the vertical centre | corner by the dominant factor about the opposite corner; edge about the opposite edge's middle | matches |
| Alt while resizing | a right edge grows symmetrically, the centre kept | resizes from the centre | matches |
| Shift and Alt on a corner | proportional, centre kept | both | matches |
| Modifier changed mid-drag | Shift on the second half locks; Shift on then off unlocks again; read fresh on every move | modifiers read on each move | matches |
| Shift while rotating | snaps the absolute angle to multiples of 15 | snapped the change in angle | fixed in #2096 (open) |
| Space while drawing or marquee | not measurable through the browser | not compared | not measured (the browser does not deliver it) |
## Nudge and arrow pan
Real keys; pan distances read from screenshots, plus or minus 3 px.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Arrow keys on a selection | nudge 1 document px, 10 with Shift, at any zoom | same | matches |
| Arrow keys with nothing selected | pan the view: Right about 62 to 64 px (two trials), Shift+Right about 320 px (5 times) | 20 and 200 px (`PAN_STEP_PX`, 10 times) | fixed in #2106 (open) |
## Zoom steps and fit
Real keys. The canvas was 1389 by 903 CSS px with the floating bottom toolbar over it.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Zoom in from 100% | 200, 400, 800, 1600, 3200, 6400, 12800, 25600 (maximum) | same steps (`STEPS` in `cmd/view.rs`) | matches |
| Zoom out from 100% | 50, 25, 13, 6, 3, 2 (minimum) | same | matches |
| Zoom from a typed 150% | in gives 200, out gives 100 (snaps to the list) | same | matches |
| Zoom to fit (Shift+1) | a 100 by 100 layer at 793% (vertical margin about 55 px); a 1000 by 200 layer at 134% (horizontal margin about 24 px each side); not a fixed 64 px margin, and not separated from the toolbar overlay | not changed | inconclusive (the toolbar overlay mixes into the margins) |
| Zoom to selection (Shift+2) | a 10 by 10 layer at 7630%, about 39 px above and about 100 px below (toolbar) | not changed | inconclusive (same overlay) |
## Snapping and menus
Measured earlier the same day; recorded here by pull request only.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Snap threshold | a layer snaps about 4.5 screen px from a guide, at 100% and 200% | snapped at 5 screen px | fixed in #2086 (open) |
| Submenu opening | a submenu opens about 11 ms after its row is hovered; no safe triangle seen | opened after 0.15 s, with a safe triangle | delay fixed in #2093 (open); the safe triangle FigCraft keeps is unresolved (none seen in Figma; asked in #2093) |
## Auto layout: Fill and Hug
Values read from the zoomed Design panel's fields and dropdowns, in horizontal rows hugging height unless noted.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Cross axis: a child set to Fill in a hugging parent | the child stretches to the tallest sibling; the parent stays Hug (child 40 Fill, parent 40 Hug; sibling typed to 60 gives 60 Fill and 60 Hug); no notice | turned the hugging parent Fixed | fixed in #2099 (open) |
| Cross axis: every child set to Fill | when the last non-Fill child is set, toast "Height set to Fixed for Frame 1" and the parent turns Fixed at its current size; typing 80 on the parent gives 80 Fixed with both children 80 Fill | the parent turned Fixed and the child kept its own size | fixed in #2099 (open) |
| Vertical stack hugging width | C (20 by 10) set to W Fill gives C 100 and the parent 100 Hug; D typed to 150 gives C 150, parent 150 Hug; no toast | the parent turned Fixed and the child kept its own size | fixed in #2099 (open) |
| Padding with a stretched child | padding 10 top and bottom, parent H 60 Hug; child H Fill gives 40 (the content box), parent stays 60 Hug | the parent turned Fixed and the child kept its own size | fixed in #2099 (open) |
| Min height on a stretched child | min height 50 gives child 50 Fill and parent 50 Hug (the stretched child's minimum counts toward the hug size) | the parent turned Fixed and the child kept its own size | fixed in #2099 (open) |
| Text sibling | rect H Fill beside two lines of text (Inter 24, line height 32): text 64 Hug, rect 64 Fill, parent 64 Hug; a third line gives 96, 96, 96 Hug | the parent turned Fixed and the child kept its own size | fixed in #2099 (open) |
| Main axis: a Fill child in a hugging parent | the parent turns Fixed (toast "Width set to Fixed for Frame 1", parent 197 Fixed) | the parent becomes fixed | matches |
| Wrap on, Fill height on a child | "Fill container" for height is greyed out on a child of a wrapping row (checked only with the parent hugging height) | not compared | not measured (only the hugging case was checked) |
| Setting a parent to Hug while a child fills along it | the child turns Fixed with the toast `Width set to Fixed for Rectangle 1` (the child's name) | demoted the child silently | fixed in #2099 (open) |
## Instances
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Main component width and height follow its content | main: horizontal auto layout, W Hug, H Hug (135 by 49); a pasted instance reads the same | instance root sizing did not follow its main | fixed in #2099 (open) |
| A child set to Fill along the main axis | toast "Width set to Fixed for Frame 1"; main W Fixed 135, H Hug; the plain instance turns W Fixed 135 too, H Hug | the instance's own resizing never followed its main | fixed in #2099 (open) |
| Instance with its width overridden to 200 | stays W 200 Fixed through the main's change, H keeps following (Hug) | the instance's own resizing never followed its main | fixed in #2099 (open) |
| Main set back to Hug contents | toast "Width set to Fixed for Rectangle 1"; main 135 Hug; the plain instance back to 135 Hug; the overridden one stays 200 Fixed | the instance's own resizing never followed its main | fixed in #2099 (open) |
| Undo after the Fill change | unreliable in the web app (reverted the child, left the main Fixed) | not compared | inconclusive (not used as evidence) |
## Components
Third entry of the sidebar header's component dropdown, several layers selected.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Create component set on frames named "Primary", "Secondary", "Ghost" | one set named "Component 1"; variants show their bare names; one property "Property 1" with the names as values, in canvas order left to right | no Create component set | fixed in #2095 (open) |
| Frames named "Size=Small", "Size=Large", "Size=Medium" | set "Component 1"; property "Size", values parsed from the names (Layers shows "Small", "Large", "Medium") | no Create component set | fixed in #2095 (open) |
| Three frames all named "Card" | set named "Card"; property "Property 1", every variant "Default"; the panel warns that some variants share property values, with "Select conflicting variants" | no Create component set | partly fixed in #2095 (open): the set and its names; the duplicate-value warning and Select conflicting variants are not added |
| Layout of the new set | no auto layout; the frames stay where they were; the set (purple dashed frame, stroke #8A38F5 inside 1 px, corner radius 5) wraps them with 20 px on every side; the set is selected afterwards; one undo reverts everything | no Create component set | fixed in #2095 (open) |
| Wrap style, caret in one paragraph | Balance re-wraps only that paragraph; with the layer selected the control shows "Mixed" | a wrap style per layer only | fixed in #2094 (open) |
| Wrap style set on the whole layer | Pretty overwrites the paragraph's Balance; editing again, both paragraphs show Pretty | a wrap style per layer only | fixed in #2094 (open) |
| Text style carrying the wrap style | not checked | not compared | not measured (not checked) |
## Number field scrubbing
Real drags on the field's label; values read from the input.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| X from 100 | a 12.2 CSS px drag right gives 106; a 61.2 CSS px drag gives 131; about 0.5 per CSS px, whole numbers | 1 per point, with a speed factor by height | fixed in #2102 (open) |
| Rotation | a 12.2 CSS px drag right on its icon gave -6 degrees (one clean run; direction to confirm) | 1 per point | fixed in #2102 (open) |
| Opacity from 100% | 12.2 CSS px left gives 95% (about 0.4 per px); clamped at 100 going right | 1 per point | fixed in #2102 (open) |
| Shift and Alt rates | synthetic events did not reach the label's handlers | ten times with Shift, a tenth with Alt | not measured (not deliverable through the browser) |
| Pointer lock | not measured | not compared | not measured |
## Sidebars
Read with `getBoundingClientRect` at a 1920 CSS px viewport, device pixel ratio 1.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Left sidebar | default 240, minimum 240, maximum about 450 (451 with the border) | 200 to 480 | fixed in #2103 (open) |
| Right sidebar | default 240 (241 with the border), minimum 240, maximum 500 | 240 to 480 | fixed in #2103 (open) |
| Edge | a double-click resets to the default; the hit zone is a couple of px wide | double-click resets | matches |
## Auto-pan at the viewport edge
Synthetic held drags at 100% zoom, pan speeds read from timed holds (0.5, 1.0, 1.5 and 3.0 s, linear in time).
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Moving a layer with the pointer near an edge | 40, 25 and 15 px inside: no pan; 12 px inside about 120 px/s; 8 px about 275 px/s; 2 px 520 to 530 px/s; 30 px outside about 600 px/s | no auto-pan for any drag | fixed in #2104 (open) |
| Fit of the speed | speed = clamp(40 x (15 - d), 0, 600) screen px per second, d = distance inside the edge (negative outside); zone about 14 to 15 px; cap about 600 px/s; no acceleration | n/a | fixed in #2104 (open) |
| Marquee, drawing a rectangle, dragging a prototype connection | the same pan (bottom, left and right edges tested) | none | fixed in #2104 (open) |
| Resize, rotate and other gestures | not measured | not compared | not measured |
| Speed at other zooms | not measured (taken as screen px per second) | not compared | not measured |
## Vector edit clipboard
Real keys and clicks, an open pen path A-B-C-D (three segments), vector edit mode entered on it.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Copy a segment, leave edit mode, paste | a new layer holding only that segment, in place, sized to it | copied the whole layer | fixed in #2105 (open) |
| Copy one point, paste | a new layer holding the segments touching the point, in place | copied the whole layer | fixed in #2105 (open) |
| Cut a segment | removed from the network; the path splits into two subpaths in the same layer; edit mode stays; pasting outside gives a new layer with the cut segment, in place | deleted the segment without copying it | fixed in #2105 (open) |
| Paste while editing the same vector | no new layer; a duplicate of the segment is added to the network in place; the pasted points are selected | did nothing | fixed in #2105 (open) |
| Paste a copied rectangle while editing a vector | the rectangle's outline joins the network in place (four corners selected); no new layer; the bounds grow to include it | did nothing | fixed in #2105 (open) |
A click on a segment's middle can add a point there in Figma's edit mode (seen once); it did not change the rules above.
## Duplicate and paste
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Ctrl+D | a copy in place, selected, just above | same | matches |
| Move the copy +100, Ctrl+D | repeats +100 (smart duplicate); a reselect forgets the step | same | matches |
| Alt-drag +100, Ctrl+D | repeats (+200 in total) | same | matches |
| Paste with the original in view | in place on it, selected, just above | same | matches |
| Paste with the original off screen | at the view's centre | same | matches |
| Paste with a frame selected (far away) | inside the frame, at its centre | same | matches |
| Tests for the three paste placement rules | the three rules above | already as measured; no test pinned them | matches (pinned by tests in #2108, open) |
| Ctrl+Shift+V (paste over) | not measured (chord blocked or slow) | paste over at the selection's top-left | not measured (chord blocked or slow) |
## Selection keys
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Escape with a nested layer selected | one Escape selects nothing, whether the layer was selected on the canvas, from the Layers panel or by Tab | selected the parent first | fixed in #2107 (open) |
## Text-editing keys
Defaults for a new text: Inter Regular 12, line height Auto, letter spacing 0%.
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Ctrl+Shift+> and < | font size +1 and -1 (12 to 18 stepped) | step 1 | matches |
| Alt+Shift+> and < | line height +1 and -1 px; from Auto at 12 it starts about 15 (not exact) | from the resolved height, 1 px | matches |
| Alt+> and < | letter spacing +1% and -1% | 1% | matches |
| Ctrl+B, I, U on a selected layer | toggles bold, italic, underline | same | matches |
| Double-click a word while editing | selects the word | same | matches |
| Triple-click | selects the line (paragraph) | same | matches |
| Escape while editing | leaves editing, the layer stays selected | same | matches |
| Ctrl+A while editing | selects all the text | same | matches |
## Open questions
| Behaviour | Figma (measured) | FigCraft | Status |
|---|---|---|---|
| Add auto layout (Shift+A) defaults | two 40 by 40 layers side by side: horizontal, gap "Auto", padding 0, W Fixed (140), H Hug (40); the same with a Y offset of 30 drops the offset; stacked: vertical, gap 60 (numeric), W Fixed (40), H Hug (140); a single 40 by 40 layer: new frame, W and H Hug (60), padding 10, gap 10; an existing 300 by 200 frame with two layers: W stays Fixed (300), H becomes Hug (149), gap "Auto", padding 49 | Hug on both axes for a wrapped selection and an inferred gap | inconclusive (inconsistent between cases in Figma itself: the gap is "Auto" in one case and numeric in another, the cross axis Fixed in the vertical case; the inference has more inputs than measured) |
Also open, as listed in their sections: the zoom-to-fit margins (zoom section, inconclusive because of the toolbar overlay), Space while drawing or dragging a selection box and Ctrl while moving (modifiers section, not measurable from the browser), and the Shift and Alt scrub rates (number field section, not measurable).
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-11-08 | minor | Created from the 2026-11-08 black-box comparison of FigCraft's interactions with Figma's, with synthetic files and no committed incumbent screenshots |