From your eye to a spec.
Drag UI elements where they look right, export the exact coordinates, and hand them to your AI coding agent.
Quickstart · How it works · Author a pack · MIT
AI coding agents write your UI, but they can't see it. Aligning a layout turns into round after round of typing positions in words:
"move the logo up a bit — no, too far — now center the button — smaller — undo that…"
You have the visual judgment. The agent has the hands. There's no clean channel between them for position.
EyeToSpec is that channel. Drag each element on a canvas until it looks right, hit export, and you get a compact coordinate JSON your agent can build against — precisely, in one pass.
No install, no build step. Just Python 3 (already on macOS and most Linux):
git clone https://github.com/sydpz/EyeToSpec.git
cd EyeToSpec
python3 serve.pyYour browser opens to the pack list. Click Search Home, drag the logo,
search bar, and button into place, then hit 💾 Save. The coordinates land
in output/search-home.json.
That file is the whole point — feed it to your agent:
"Lay out these elements using the normalized coordinates in
output/search-home.json(cx/cy are the element center as a fraction of the canvas; w/h are size as a fraction of the canvas)."
An asset pack is a folder in config/ describing a canvas and the elements
to place on it. EyeToSpec renders them, you arrange them, and it exports where
they ended up — as normalized coordinates.
Why normalized (0–1) coordinates? Because they're resolution- and
device-independent. cx: 0.5 means "horizontally centered" whether the target
is a 720px phone screen or a 4K display. The agent multiplies by the real canvas
size at build time.
{
"logo": { "cx": 0.5, "cy": 0.18, "w": 0.45, "h": 0.13 },
"searchbar": { "cx": 0.5, "cy": 0.34, "w": 0.72, "h": 0.06 },
"btn_search":{ "cx": 0.38, "cy": 0.46, "w": 0.24, "h": 0.05 }
}cx/cy— element center, as a fraction of canvas width / height.w/h— display size, as a fraction of canvas width / height.
Flat and literal: what you drag is exactly what lands in the JSON. No grouping, no folding, nothing to decode.
Beyond position and size, each element can be rotated (drag the top handle) and flipped horizontally or vertically (the ↔ / ↕ toggles in the inspector). These only appear in the export when they're set, so simple layouts stay clean:
{
"btn_search": { "cx": 0.3, "cy": 0.52, "w": 0.3, "rotation": 60, "flipH": true, "flipV": true }
}rotation— degrees clockwise (drag snaps to 15°; hold Shift for free rotation).flipH/flipV— mirror left↔right / top↔bottom. Only emitted whentrue.
Flip matters when art has a direction: a sprite drawn facing right can't be rotated to face left without turning upside down — it has to be mirrored.
| Kind | In pack.json |
In the editor |
|---|---|---|
| Image | "file": "logo.svg" |
scales by width, height locked to aspect |
| Text | "file": null, "text": "Sign in" |
renders the real copy (WYSIWYG) at its font size/color |
| Box | "file": null, no text |
a labeled placeholder you size freely (for code-drawn elements) |
EyeToSpec does one thing: turn your visual judgment into a coordinate spec.
It deliberately does not cut your assets (that's your job — screenshot, export, or draw them) and does not wire the JSON into your code (that's your agent's job). One folder in, one JSON out. That narrowness is the point: it stays small, predictable, and easy to trust.
- Make a folder under
config/, e.g.config/login-screen/. - Drop your cut assets in
config/login-screen/assets/. - Write a
pack.jsondescribing the canvas and elements. - Restart
serve.py— your pack appears in the list.
Want to align a real page? Screenshot any site, slice out the pieces you care about, drop them in a pack, and rebuild the layout by eye. The exported coordinates are yours to hand off.
See docs/pack-format.md for the full manifest format.
serve.py binds to your LAN by default, so you can drag a layout from a phone
on the same wifi:
python3 serve.py # note the "phone" URL it printsOpen that URL on your phone — drag and resize work with touch.
Note: the default
--host 0.0.0.0exposes the server to everyone on your local network (there's no auth — it's a personal dev tool). That's fine on a home/office wifi; on an untrusted network use--host 127.0.0.1for local-only access, or an SSH tunnel.
python3 serve.py --port 8771 # use a different port
python3 serve.py --no-open # don't auto-open the browser
python3 serve.py --host 127.0.0.1 # local only (no phone access)The GIF above is scripted, not hand-recorded — see
docs/media/record-demo.js (Playwright + ffmpeg).
It's dev-only tooling; the tool itself has zero runtime dependencies.
MIT — see LICENSE.
