@impractical-ai/motion 0.1.16 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/motion-standard.md +46 -0
- package/dist/{api-V7Q3PIWY.js → api-LZ6H5LHP.js} +2 -2
- package/dist/{chunk-UEPY33MB.js → chunk-4SKYR3FS.js} +8 -49
- package/dist/{chunk-CJLGKZMD.js → chunk-5VN5GZWK.js} +1 -1
- package/dist/chunk-D3OOI7XP.js +111 -0
- package/dist/{stdio-R4RCWJIS.js → chunk-MTNMG25I.js} +37927 -70070
- package/dist/chunk-QQ6NYC74.js +14552 -0
- package/dist/chunk-W4VH5SCR.js +18083 -0
- package/dist/chunk-XPFY56XH.js +71 -0
- package/dist/chunk-ZJSSF74D.js +51 -0
- package/dist/commands-FSL4FNI4.js +468 -0
- package/dist/draft-check-YWT3WVD3.js +53 -0
- package/dist/guidance-LQX2JZWK.js +113 -0
- package/dist/index.js +30 -10
- package/dist/stdio-643H2TXL.js +23 -0
- package/dist/template-source-XWVFNS6W.js +19 -0
- package/package.json +3 -3
- package/dist/chunk-AP27EKRR.js +0 -71
- package/dist/commands-6F7HITZN.js +0 -1683
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Arrival, hold and exit — house motion standard
|
|
2
|
+
|
|
3
|
+
Adopted by explicit user direction on 2026-10-19. It supersedes the 2026-09-26 "arrival and residual drift" rule (the Rox `ambientOffset` drift layer), which is withdrawn. Applies to newly authored or revised video, Atelier and reusable motion components.
|
|
4
|
+
|
|
5
|
+
## Default behavior
|
|
6
|
+
|
|
7
|
+
**Each foreground element follows one continuous trajectory: it arrives with energy, decelerates strongly to near-zero speed for the reading hold, then accelerates into its exit or its next authored move.**
|
|
8
|
+
|
|
9
|
+
- The entrance and exit eases carry the whole shape. There is no separate drift layer, no constant velocity floor and no perpetual creep.
|
|
10
|
+
- **Finish the visible travel quickly.** During the hold, the remaining travel of the entrance's ease tail must be under about 0.5 output px in total. The subject then reads as calm and still, not creeping. A long-tailed out-ease is fine as long as its last stretch covers less than half a pixel.
|
|
11
|
+
- **Exits accelerate out of the near-zero state** (an in or in-out ease). Nothing starts from a hard stop, and the tail of the arrival flows into the exit without a velocity discontinuity.
|
|
12
|
+
- **Whole entities, not every element.** Arrival, hold and exit apply to the larger entity on screen: a card, a product window, a headline block, a logo lockup. Its children (labels, icons, rows, words, buttons) ride with it inside the same transform. Do not recurse and give each child its own arrival, deceleration and exit: that reads as fidgeting and splits attention. Children may still have their own discrete actions inside the hold (text typing in, a toggle flipping, a row highlighting, a number counting), but not their own arrive/hold/exit motion.
|
|
13
|
+
- “Hold”, “settled”, “landed” and “reading time” mean visually still, stable and readable. If a long hold needs life, add a discrete event (a click, a reveal, a state change), not a drift.
|
|
14
|
+
|
|
15
|
+
### Why no creep
|
|
16
|
+
|
|
17
|
+
Exports render in headless Linux Chromium, which snaps slowly moving text and elements to whole pixels. A slow drift does not glide: the subject sits still for many frames, then jumps a full pixel in one frame. At about 1 px/s that is a visible 1 px tick every ~0.9 s. This was measured on 2026-10-19 on a Slack-agent export (text holding for ~50 frames, then jumping ~0.7 px, repeatedly) and reproduced in isolation in Linux Chromium (1.0 px single-frame snaps). Mac Chrome positions text at fractional pixels, so the Studio preview can look smooth while the export ticks.
|
|
18
|
+
|
|
19
|
+
## Ownership and grouping
|
|
20
|
+
|
|
21
|
+
- The component controls foreground objects only. The host composition owns its environment. Never animate, zoom, rotate, translate, retime, pulse or otherwise modify a background merely to give a foreground component motion. A host-authored background may have its own independent clock.
|
|
22
|
+
- Keep the environment outside the subject transform hierarchy. Prefer transparent reusable foreground exports; Atelier preview backgrounds are separate host layers.
|
|
23
|
+
- Move a semantic assembly as one group: object, attached text, controls, caret, connector endpoints and contact shadow. The group is what arrives, holds and exits; its parts don't each get their own entrance.
|
|
24
|
+
- Make things breathe: one primary focus at a time, with generous negative space and comfortable margins around it. Keep supporting context subordinate, and sequence competing text and actions so each lands and is read before the next. If crowded, remove or sequence content before shrinking it. Camera pushes and reframing may serve that focus; one focus does not require a locked camera. Do not use a generic card as the default container for every concept.
|
|
25
|
+
- Preserve 3D hierarchy, object identity and attached geometry.
|
|
26
|
+
|
|
27
|
+
## Determinism
|
|
28
|
+
|
|
29
|
+
Derive every pose from the existing seekable composition clock. No wall-clock timers, unseeded randomness, infinite animation loops or accumulated per-frame deltas. Rewinding must reproduce the same pose.
|
|
30
|
+
|
|
31
|
+
## Exceptions
|
|
32
|
+
|
|
33
|
+
Genuine continuous travel is authored motion, not a hold: a ticker, a river of cards, a footage scrub or a deliberate camera move. Keep it fast enough to read as motion (well above a few px/s), or make it a cut. Historical exports are reference evidence, not assets to rewrite merely because the standard changed.
|
|
34
|
+
|
|
35
|
+
## Review and acceptance
|
|
36
|
+
|
|
37
|
+
1. Inspect the moving foreground at entrance, deceleration, hold and exit, in an export (not only the Studio preview).
|
|
38
|
+
2. In a reading hold, sample two frames 0.3–0.5 s apart: the held subject should not have moved by a visible amount (under ~0.5 px).
|
|
39
|
+
3. Confirm the exit accelerates out of the hold rather than jumping from a dead stop.
|
|
40
|
+
4. Verify attached labels, controls, shadows and projected anchors keep their relationships, with safe margins and readability throughout.
|
|
41
|
+
5. Verify object animation does not change the host background's framing or state.
|
|
42
|
+
6. Check repeated, reverse and retimed seeks.
|
|
43
|
+
|
|
44
|
+
## Maintenance and distribution
|
|
45
|
+
|
|
46
|
+
This document is canonical. The director playbook and motion-direction skill carry the operative rule; AGENTS.md, CLAUDE.md, Atelier guidance and repository entry points link here. The CLI bootstrap skill includes a portable summary because installed agents may not have the repository. Its source is `cli/assets/impractical/SKILL.md`; `scripts/build-claude-plugin.mjs` synchronizes the repository skill and plugin copy. `scripts/build-motion-library.mjs` embeds the playbook and authoring skills into the served motion library. MCP initialization instructions also state the default.
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
waitForJobs,
|
|
18
18
|
withAdmissionReceipt,
|
|
19
19
|
withMeteredCall
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import "./chunk-
|
|
20
|
+
} from "./chunk-5VN5GZWK.js";
|
|
21
|
+
import "./chunk-XPFY56XH.js";
|
|
22
22
|
import "./chunk-77HVPD4G.js";
|
|
23
23
|
export {
|
|
24
24
|
AGENT_HEADER,
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
DEVICE_START_BODY,
|
|
4
|
+
fetchWorkspaces,
|
|
5
|
+
grantScope,
|
|
6
|
+
workspaceLabel
|
|
7
|
+
} from "./chunk-ZJSSF74D.js";
|
|
8
|
+
import {
|
|
4
9
|
ApiError,
|
|
5
10
|
delay,
|
|
6
11
|
invokeAt,
|
|
7
12
|
loadConfig,
|
|
8
13
|
recoverCorruptConfig,
|
|
9
14
|
saveConfig
|
|
10
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-5VN5GZWK.js";
|
|
11
16
|
import {
|
|
12
17
|
PACKAGE_COMMAND,
|
|
13
18
|
PACKAGE_NAME,
|
|
14
19
|
PACKAGE_VERSION,
|
|
15
20
|
PRODUCTION_APP_URL,
|
|
16
21
|
PRODUCTION_SUPABASE_URL
|
|
17
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-XPFY56XH.js";
|
|
18
23
|
|
|
19
24
|
// src/onboarding.ts
|
|
20
25
|
import {
|
|
@@ -32,48 +37,6 @@ import { dirname, join } from "node:path";
|
|
|
32
37
|
import { fileURLToPath } from "node:url";
|
|
33
38
|
import { spawnSync } from "node:child_process";
|
|
34
39
|
import { createRequire } from "node:module";
|
|
35
|
-
|
|
36
|
-
// src/workspaces.ts
|
|
37
|
-
var UNSUPPORTED = {
|
|
38
|
-
supported: false,
|
|
39
|
-
keyScope: null,
|
|
40
|
-
lastOpenWorkspaceId: null,
|
|
41
|
-
workspaces: []
|
|
42
|
-
};
|
|
43
|
-
async function fetchWorkspaces(request) {
|
|
44
|
-
let result;
|
|
45
|
-
try {
|
|
46
|
-
result = await request("workspace-bootstrap", { action: "list_workspaces" });
|
|
47
|
-
} catch (error) {
|
|
48
|
-
if (error instanceof ApiContractError) return UNSUPPORTED;
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
if (!Array.isArray(result?.workspaces)) return UNSUPPORTED;
|
|
52
|
-
const keyScope = result.keyScope === "user" ? "user" : result.keyScope === "org" ? "org" : null;
|
|
53
|
-
const workspaces = result.workspaces.filter((row) => typeof row?.id === "string").map((row) => ({
|
|
54
|
-
id: String(row.id),
|
|
55
|
-
name: typeof row.name === "string" ? row.name : null,
|
|
56
|
-
slug: typeof row.slug === "string" ? row.slug : null,
|
|
57
|
-
role: typeof row.role === "string" ? row.role : "",
|
|
58
|
-
plan: typeof row.plan === "string" ? row.plan : null,
|
|
59
|
-
usage: row.usage && typeof row.usage === "object" ? row.usage : null
|
|
60
|
-
}));
|
|
61
|
-
return {
|
|
62
|
-
supported: true,
|
|
63
|
-
keyScope,
|
|
64
|
-
lastOpenWorkspaceId: typeof result.lastOpenWorkspaceId === "string" ? result.lastOpenWorkspaceId : null,
|
|
65
|
-
workspaces
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function workspaceLabel(workspace) {
|
|
69
|
-
return workspace.name ? `${workspace.name} (${workspace.id})` : workspace.id;
|
|
70
|
-
}
|
|
71
|
-
var DEVICE_START_BODY = { action: "start", scope: "user" };
|
|
72
|
-
function grantScope(grant) {
|
|
73
|
-
return grant.scope === "user" ? "user" : "org";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// src/onboarding.ts
|
|
77
40
|
var CLAUDE_MARKETPLACE = "Openpod/impractical-agent-plugins";
|
|
78
41
|
var CLAUDE_PLUGIN = "impractical-motion@impractical";
|
|
79
42
|
var CLAUDE_MARKETPLACE_NAME = "impractical";
|
|
@@ -817,10 +780,6 @@ async function setup(options) {
|
|
|
817
780
|
}
|
|
818
781
|
|
|
819
782
|
export {
|
|
820
|
-
fetchWorkspaces,
|
|
821
|
-
workspaceLabel,
|
|
822
|
-
DEVICE_START_BODY,
|
|
823
|
-
grantScope,
|
|
824
783
|
openBrowser,
|
|
825
784
|
setup
|
|
826
785
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
external_exports
|
|
4
|
+
} from "./chunk-QQ6NYC74.js";
|
|
5
|
+
import {
|
|
6
|
+
invoke
|
|
7
|
+
} from "./chunk-5VN5GZWK.js";
|
|
8
|
+
|
|
9
|
+
// src/mcp/template-source.ts
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, resolve, sep } from "node:path";
|
|
12
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,119}$/;
|
|
13
|
+
async function fetchTemplate(slug, call) {
|
|
14
|
+
if (!SLUG.test(slug)) throw new Error(`"${slug}" isn't a template slug (lowercase letters, digits and dashes)`);
|
|
15
|
+
return call("atelier-library", { action: "film-source", slug });
|
|
16
|
+
}
|
|
17
|
+
var readingOrder = (paths) => [...paths].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
18
|
+
var rank = (path) => path === "direction.ts" ? 0 : path === "App.tsx" ? 1 : path.startsWith("kit/") ? 3 : 2;
|
|
19
|
+
var templateSourceTool = {
|
|
20
|
+
name: "template_source",
|
|
21
|
+
title: "Read a template\u2019s code",
|
|
22
|
+
description: "The source of a template from the Impractical template library (by its slug, as the Studio or a prompt names it): direction.ts (its choreography: timing, eases, camera, transitions), App.tsx (its set and components) and the rest of its files. Call it whenever someone gives you a template to work from, and study how it is built and moves before you plan. Adapt its structure and motion to the new video; never copy its content.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
slug: external_exports.string().describe('The template\u2019s slug, e.g. "framer-cms-list-replica".')
|
|
25
|
+
},
|
|
26
|
+
// read-only library browsing: admitted (unmetered) as the library's other lookups are
|
|
27
|
+
admitAs: "atelier_study",
|
|
28
|
+
handler: async (args, ctx) => {
|
|
29
|
+
const source = await fetchTemplate(String(args.slug ?? ""), ctx.invoke);
|
|
30
|
+
const paths = readingOrder(Object.keys(source.files));
|
|
31
|
+
const kits = paths.filter((p) => p.startsWith("kit/"));
|
|
32
|
+
const shown = paths.filter((p) => !p.startsWith("kit/"));
|
|
33
|
+
return {
|
|
34
|
+
text: `TEMPLATE ${args.slug}: ${paths.length} file(s). Read direction.ts first (how it moves), then App.tsx (what it shows).` + (kits.length ? ` Kits it uses (install_kits provides them): ${kits.join(", ")}.` : "") + "\n\n" + shown.map((p) => `=== ${p} ===
|
|
35
|
+
${source.files[p]}`).join("\n\n")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var TEMPLATE_PAGE = 25;
|
|
40
|
+
function rankTemplates(items, query) {
|
|
41
|
+
const films = items.filter((item) => item.kind === "film");
|
|
42
|
+
const words = query.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 1);
|
|
43
|
+
if (!words.length) return films;
|
|
44
|
+
const score = (item) => {
|
|
45
|
+
const name = `${item.title} ${item.slug}`.toLowerCase();
|
|
46
|
+
const about = `${item.creator ?? ""} ${item.description ?? ""}`.toLowerCase();
|
|
47
|
+
return words.reduce((sum, word) => sum + (name.includes(word) ? 3 : 0) + (about.includes(word) ? 1 : 0), 0);
|
|
48
|
+
};
|
|
49
|
+
return films.map((item) => ({ item, score: score(item) })).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).map((x) => x.item);
|
|
50
|
+
}
|
|
51
|
+
var templateSearchTool = {
|
|
52
|
+
name: "template_search",
|
|
53
|
+
title: "Find a template",
|
|
54
|
+
description: 'Search the Impractical template library (the Studio\u2019s templates: complete films, many of them replicas of real launch films such as Notion Mail, Attio or Framer) by name or words, and get each match\u2019s slug. Use it when someone mentions a template, a film in the library, or "the X one", then read the one they mean with template_source. Leave the query empty to list them all. Results come a page at a time: when there are more, the reply says so and gives the `after` to pass for the next page.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
query: external_exports.string().max(200).optional().describe('Words from the template\u2019s name or what it shows, e.g. "notion mail" or "attio sequences".'),
|
|
57
|
+
after: external_exports.number().int().min(0).optional().describe("How many results to skip: the `after` a previous reply gave, for its next page.")
|
|
58
|
+
},
|
|
59
|
+
// read-only library browsing: admitted (unmetered) as the library's other lookups are
|
|
60
|
+
admitAs: "atelier_study",
|
|
61
|
+
handler: async (args, ctx) => {
|
|
62
|
+
const { items } = await ctx.invoke("atelier-library", { action: "catalog" });
|
|
63
|
+
const query = String(args.query ?? "");
|
|
64
|
+
const all = rankTemplates(items ?? [], query);
|
|
65
|
+
if (!all.length) {
|
|
66
|
+
return { text: `No template matches "${query}". Call template_search with no query to see them all.` };
|
|
67
|
+
}
|
|
68
|
+
const after = Math.min(Math.max(0, Number(args.after ?? 0) || 0), all.length);
|
|
69
|
+
const found = all.slice(after, after + TEMPLATE_PAGE);
|
|
70
|
+
if (!found.length) {
|
|
71
|
+
return { text: `That's past the last of the ${all.length} template(s)${query ? ` for "${query}"` : ""}. Call template_search without \`after\` to start over.` };
|
|
72
|
+
}
|
|
73
|
+
const lines = found.map(
|
|
74
|
+
(item) => `- ${item.slug}: ${item.title}${item.durationSeconds ? ` (${Math.round(item.durationSeconds)}s)` : ""}` + (item.description ? ` \u2014 ${item.description.slice(0, 200)}` : "")
|
|
75
|
+
);
|
|
76
|
+
const next = after + found.length;
|
|
77
|
+
const more = next < all.length ? `
|
|
78
|
+
|
|
79
|
+
Showing ${after + 1}\u2013${next} of ${all.length}. ${all.length - next} more: template_search({ ${query ? `query: ${JSON.stringify(query)}, ` : ""}after: ${next} }).` : after > 0 ? `
|
|
80
|
+
|
|
81
|
+
Showing ${after + 1}\u2013${next} of ${all.length}, the last page.` : "";
|
|
82
|
+
return {
|
|
83
|
+
text: `${all.length} template(s)${query ? ` for "${query}"` : ""}, best match first:
|
|
84
|
+
${lines.join("\n")}${more}
|
|
85
|
+
|
|
86
|
+
Read the one meant with template_source({ slug }).`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
async function templateSourceCommand(slug, dir) {
|
|
91
|
+
if (!slug || !dir) throw new Error("Usage: impractical template-source <slug> --dir <folder>");
|
|
92
|
+
const source = await fetchTemplate(slug, invoke);
|
|
93
|
+
const root = resolve(dir);
|
|
94
|
+
const written = [];
|
|
95
|
+
for (const path of readingOrder(Object.keys(source.files))) {
|
|
96
|
+
const target = resolve(root, path);
|
|
97
|
+
if (!target.startsWith(root + sep)) continue;
|
|
98
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
99
|
+
writeFileSync(target, source.files[path]);
|
|
100
|
+
written.push(path);
|
|
101
|
+
}
|
|
102
|
+
return written.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
templateSourceTool,
|
|
107
|
+
TEMPLATE_PAGE,
|
|
108
|
+
rankTemplates,
|
|
109
|
+
templateSearchTool,
|
|
110
|
+
templateSourceCommand
|
|
111
|
+
};
|