The First 60 Seconds

Somewhere between the 3D work and the PBR pipeline, Lux picked up the shape of a serious creative coding tool. It had a render stack. It had an SDF graph compiler. It had particles, materials, IBL, post-FX. What it didn’t have was a good way to start using it. Which I think matters more than any of the above, though I’ll admit the “onboarding is a first-class feature” argument is one I’ve seen people make confidently in both directions.

If you installed Lux a month ago and ran it for the first time, here’s what happened: a blank canvas opened. A hardcoded 3D triangle sat in the middle of the output. No hint about what any of it meant. No instructions. No “do this first”. You could close the app or you could start reverse-engineering the editor, and neither felt great.

That’s the thing this session fixed. Not with more features, but with everything around the edges: a welcome splash, a sample patch, a menu bar, a help overlay, and a preferences file to remember your choices.

The bar I gave myself was the one I’ve been holding all of Lux to: a first-time user should be productive in under 60 seconds. This post is the shape of those 60 seconds.

The triangle has to go

The startup demo used to be a hardcoded five-node scene: triangle mesh, unlit material, ortho camera, RenderScene, TextureToLayer. No interactivity. No cursor. No hint of what makes Lux Lux. It existed because back in the first 3D post I needed proof-of-life and a triangle is the minimum-viable thing that proves the 3D pipeline works.

It also directly violated a thing I’ve been repeating to myself: “Connecting a Mouse to a Circle should just work.” Connecting a mouse to a circle is supposed to be the first thing that works, because that’s the moment you feel the difference between a tool and an IDE. A static triangle is an IDE.

The new default is a two-node graph. io.Mouse → shape.Circle, wired Mouse.position → Circle.center. Move your mouse. The circle follows. That’s it. First frame. No reading involved.

It ships as app/assets/samples/hello_mouse_circle.lux, loaded via include_str! into the binary and parsed through the regular project-load path. Two tests back it up: one that verifies the bundled bytes parse into the expected shape (2 nodes, 1 wire, right plugin types, right pin names); one that reconstructs the sample against the full plugin registry and asserts the wire survives load. If the sample ever stops round-tripping, CI fails.

The reason to ship a real .lux file rather than build the graph in Rust is that the sample has to look like something a user could have made. If you save this, send it to a friend, and they open it, it’s the same bytes. If you inspect it with cat, it’s readable JSON. The sample isn’t a demo hiding behind the demo path — it’s a patch.

The welcome splash

First thing you see on first launch is a centred modal with a dimmed backdrop. Three options:

  1. “Hello Mouse → Circle” — the sample above, ready to click into and explore.
  2. Empty canvas — start from zero.
  3. Open project… — route through the native file dialog to load an existing .lux file.

Plus three shortcut chips at the bottom: Space (create dialog), Tab (fuzzy search at cursor), Shift Shift (search everywhere). Because the whole keyboard-first interaction model is invisible otherwise, and a first-time user needs to know the shortcuts exist before they can learn them.

Gating is tied to a new UserPrefs struct. has_opened_before: bool starts false; the first time the user picks any option, it flips to true and gets persisted. Subsequent launches skip the splash. No one has to hit it twice.

The hover animation on the template cards uses the new motion.rs module — one place where every editor animation lives, so tweens are consistent across the UI. The card tint eases in over 140ms on hover, same curve everywhere.

There’s also a reduce_motion pref that disables every UI animation globally. Accessibility work the app was missing. Set once, every motion call respects it.

UserPrefs

UserPrefs is the persistence bucket for anything that should survive across launches and isn’t part of a specific project. It lives at ~/.config/lux/prefs.json (with the usual platform-appropriate locations on macOS and Windows via the dirs crate). Current fields:

pub struct UserPrefs {
    pub has_opened_before: bool,
    pub last_template_opened: Option<String>,
    pub reduce_motion: bool,
    pub recent_files: Vec<PathBuf>,    // from the trust pass
    // ...
}

The file is loaded in LuxApp::new and saved on every mutation. Small enough to not worry about write frequency. If the file doesn’t exist, defaults get used and the splash shows.

recent_files from the trust pass got moved into UserPrefs. It used to be its own file; having two adjacent JSON files in the config directory was annoying, and the merge simplified the loading code. Everything user-scoped lives in one file now.

The menu bar

I went back and forth on whether to add a menu bar. Lux is keyboard-first; menus are for discovery. But I kept meeting people who couldn’t find the “save” command because they didn’t know about Ctrl+S. Shortcuts are for flow; menus are the ramp onto the flow.

So there’s a menu bar now. File, Edit, View, Window, Help. Every entry shows the shortcut next to it — which teaches the shortcuts passively while serving the menu’s job. After a week of using the menus, most people never open them again because the shortcuts are in muscle memory.

The content is the obvious set:

  • File: New, Open, Recent Files ▶ (submenu), Save, Save As, Export Image, Export Sequence, Project Settings, Exit
  • Edit: Undo, Redo, Cut, Copy, Paste, Duplicate, Select All, Delete, Find Nodes…
  • View: Toggle Browser, Toggle Inspector, Toggle Output, Output Mode ▶
  • Window: Project Settings, Cheatsheet, Welcome
  • Help: Cheatsheet, Documentation, About

Every entry either fires a EditorAction (the unified command enum — see the app decomposition post) or opens a modal. No menu entry does real work inline. The menu is a dispatcher, nothing more.

The ? cheatsheet

The menu is for people who scan. The cheatsheet is for people who want the shortcut reference in one place.

Press ? (shift+/) and a modal overlay appears with every shortcut in Lux grouped by category: Canvas, Editing, File, View, Output, Debug. Two columns, monospace font, keyboard glyphs styled like keys. Escape to close.

The cheatsheet isn’t generated from a static string. It reads from a central SHORTCUTS registry that also drives the actual key handlers, so the displayed list is guaranteed to match what actually works. Add a new shortcut, update one table, the cheatsheet and the handler both see it. No drift.

The registry is a slice of (keys, description, category) tuples. Tooling could export it to a docs page; for now it just feeds the modal.

Empty-canvas CTA

If you pick “Empty canvas” from the splash, or you delete every node in a project, you end up staring at a blank canvas with no context. The old behaviour was: blank canvas, good luck. The new behaviour is a single line of text in the middle of the viewport:

Press Space to create your first node

That’s it. One line. Fades in after 500ms of emptiness. Fades out the moment you press anything or create a node.

It’s the smallest possible onboarding affordance: “this thing does nothing right now, here’s the one key that will make it do something.” Nobody reads long tutorials on first launch. Nobody needs a wall of instructions. They need a prompt.

The line is intentionally generic — “create your first node” rather than “create a Circle” — because steering a first-time user toward a specific node would be presumptuous. Space opens the Create dialog, they browse categories, they pick something that appeals, they get their first hit of “I made something.” That’s the experience. Not “follow the script.”

The through-line

Every change in this session was a small one. The sample patch is ~50 lines of JSON. The welcome modal is 280 lines of Rust. The menu bar is a few hundred lines. The cheatsheet is a table-driven modal. UserPrefs is a struct with ~5 fields.

None of them are individually exciting. All of them together close the gap between “Lux opens” and “I’m using Lux.”

The 60-second test: install Lux fresh, click it. Welcome modal appears. Click “Hello Mouse → Circle.” The sample loads. Move your mouse. The circle moves. Press ? to see what else you can do. Press Space to try another node. Thirty seconds in, you’re wiring things.

That’s the whole bar. And for the first time, Lux passes it.

Next two posts are the editor refinements that landed in the same session: wires that help you connect (magnet snap, pin-type search, backward routing), and tooltips, error dots, and the F8 profiler.


I have no idea what I’m doing or if any of this is right, but it’s fun. Follow along.

← Back to devlog