@pixerate/editor-react 0.44.0 → 0.46.0
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/dist/animation-editor/index.cjs +870 -11
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +108 -8
- package/dist/animation-editor/index.d.ts +108 -8
- package/dist/animation-editor/index.js +864 -11
- package/dist/animation-editor/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -48,22 +48,28 @@ function useAnimationEditorTools(registry, controller, options = {}) {
|
|
|
48
48
|
|
|
49
49
|
// src/animation-editor/AnimationStage.tsx
|
|
50
50
|
import { useMemo as useMemo2 } from "react";
|
|
51
|
+
import { highlightMotionSvg, motionNodeAtElement } from "@pixerate/editor/animation-editor";
|
|
51
52
|
import { jsx } from "react/jsx-runtime";
|
|
52
|
-
function AnimationStage({ controller, values, onionSkin, motionPaths, className = "" }) {
|
|
53
|
+
function AnimationStage({ controller, values, onionSkin, motionPaths, onPick, highlight, className = "" }) {
|
|
53
54
|
const state = useAnimationEditorState(controller);
|
|
54
55
|
const { scene, time } = state;
|
|
56
|
+
const key = highlight?.join("\0") ?? "";
|
|
55
57
|
const svg = useMemo2(
|
|
56
|
-
() =>
|
|
57
|
-
|
|
58
|
+
() => highlightMotionSvg(
|
|
59
|
+
controller.toSvg(time, { pretty: false, idPrefix: "ae-", values, onionSkin, motionPaths }).replace(/<svg ([^>]*?)width="[^"]*" height="[^"]*"/, '<svg $1width="100%" height="100%"'),
|
|
60
|
+
highlight ?? []
|
|
61
|
+
),
|
|
62
|
+
[controller, scene, time, values, onionSkin, motionPaths, key]
|
|
58
63
|
);
|
|
59
64
|
return /* @__PURE__ */ jsx(
|
|
60
65
|
"div",
|
|
61
66
|
{
|
|
62
|
-
className: `relative overflow-hidden ${className}`,
|
|
67
|
+
className: `relative overflow-hidden ${onPick ? "cursor-crosshair" : ""} ${className}`,
|
|
63
68
|
style: { aspectRatio: `${scene.artwork.width} / ${scene.artwork.height}` },
|
|
64
69
|
"data-animation-stage": "",
|
|
65
70
|
role: "img",
|
|
66
71
|
"aria-label": `${scene.name} at ${(time / 1e3).toFixed(2)} s`,
|
|
72
|
+
onClick: onPick ? (e) => onPick(motionNodeAtElement(scene, e.target, e.currentTarget), e) : void 0,
|
|
67
73
|
dangerouslySetInnerHTML: { __html: svg }
|
|
68
74
|
}
|
|
69
75
|
);
|
|
@@ -140,19 +146,866 @@ function ShotTimeline({ controller, className = "" }) {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
// src/animation-editor/AnimationEditor.tsx
|
|
143
|
-
import {
|
|
149
|
+
import { useMemo as useMemo6, useState as useState6 } from "react";
|
|
150
|
+
import { indexMotionNodes, motionIssueLine } from "@pixerate/editor/animation-editor";
|
|
151
|
+
|
|
152
|
+
// src/animation-editor/CommandInspector.tsx
|
|
153
|
+
import React3, { useMemo as useMemo4, useState } from "react";
|
|
154
|
+
import {
|
|
155
|
+
describeMotionElements,
|
|
156
|
+
motionCommandFields,
|
|
157
|
+
motionFieldEdit,
|
|
158
|
+
motionFlairPrimitive
|
|
159
|
+
} from "@pixerate/editor/animation-editor";
|
|
160
|
+
|
|
161
|
+
// src/animation-editor/EasePicker.tsx
|
|
162
|
+
import { useMemo as useMemo3 } from "react";
|
|
163
|
+
import { MOTION_EASE_CHOICES, motionEaseKey, motionEasingCurve, motionEasingGuides, motionEasingPath } from "@pixerate/editor/animation-editor";
|
|
144
164
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
var W = 72;
|
|
166
|
+
var H = 44;
|
|
167
|
+
function EasePicker({ value, onChange, disabled, className = "" }) {
|
|
168
|
+
const key = motionEaseKey(value);
|
|
169
|
+
const curve = useMemo3(() => motionEasingCurve(value ?? "ease-out"), [value]);
|
|
170
|
+
const guides = motionEasingGuides(curve, H);
|
|
171
|
+
const spring = typeof value === "string" ? value.startsWith("spring") : value?.type === "spring";
|
|
172
|
+
return /* @__PURE__ */ jsxs2("div", { className: `flex items-center gap-2 ${className}`, "data-ease-picker": "", children: [
|
|
173
|
+
/* @__PURE__ */ jsxs2(
|
|
174
|
+
"select",
|
|
175
|
+
{
|
|
176
|
+
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
177
|
+
"aria-label": "Ease",
|
|
178
|
+
value: key,
|
|
179
|
+
disabled,
|
|
180
|
+
onChange: (e) => {
|
|
181
|
+
if (e.target.value === "custom") return;
|
|
182
|
+
onChange(e.target.value ? e.target.value : void 0);
|
|
183
|
+
},
|
|
184
|
+
children: [
|
|
185
|
+
MOTION_EASE_CHOICES.map((c) => /* @__PURE__ */ jsx3("option", { value: c.value, children: c.label }, c.value)),
|
|
186
|
+
key === "custom" && /* @__PURE__ */ jsx3("option", { value: "custom", children: "Custom" })
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
/* @__PURE__ */ jsxs2("svg", { width: W, height: H, viewBox: `0 0 ${W} ${H}`, className: "shrink-0 rounded border border-border", "data-ease-curve": key || "default", "aria-hidden": "true", children: [
|
|
191
|
+
/* @__PURE__ */ jsx3("line", { x1: 0, x2: W, y1: guides.zero, y2: guides.zero, stroke: "currentColor", strokeOpacity: 0.2 }),
|
|
192
|
+
/* @__PURE__ */ jsx3("line", { x1: 0, x2: W, y1: guides.one, y2: guides.one, stroke: "currentColor", strokeOpacity: 0.2, strokeDasharray: "2 2" }),
|
|
193
|
+
/* @__PURE__ */ jsx3("path", { d: motionEasingPath(curve, W, H), fill: "none", stroke: "#f43f5e", strokeWidth: 1.5 })
|
|
194
|
+
] }),
|
|
195
|
+
spring && /* @__PURE__ */ jsxs2("span", { className: "text-[10px] tabular-nums text-muted-foreground", children: [
|
|
196
|
+
Math.round(curve.durationMs),
|
|
197
|
+
" ms"
|
|
198
|
+
] })
|
|
199
|
+
] });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/animation-editor/CommitInput.tsx
|
|
203
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
204
|
+
function CommitInput({ value, onCommit, ...rest }) {
|
|
205
|
+
return /* @__PURE__ */ jsx4(
|
|
206
|
+
"input",
|
|
207
|
+
{
|
|
208
|
+
...rest,
|
|
209
|
+
defaultValue: value,
|
|
210
|
+
onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(),
|
|
211
|
+
onBlur: (e) => {
|
|
212
|
+
if (e.currentTarget.value !== value) onCommit(e.currentTarget.value);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
value
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/animation-editor/CommandInspector.tsx
|
|
220
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
221
|
+
var input = "w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs";
|
|
222
|
+
function CommandInspector({ controller, picked, className = "" }) {
|
|
223
|
+
const state = useAnimationEditorState(controller);
|
|
224
|
+
const { scene, selection, readOnly, error } = state;
|
|
225
|
+
const [local, setLocalState] = useState(null);
|
|
226
|
+
const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
|
|
227
|
+
const elements = useMemo4(() => describeMotionElements(scene), [scene.artwork]);
|
|
228
|
+
const shot = selection ? scene.shots.find((s) => s.id === selection.shot) : void 0;
|
|
229
|
+
const index = selection?.command;
|
|
230
|
+
const command = shot && index !== void 0 ? shot.run[index] : void 0;
|
|
231
|
+
if (!shot || index === void 0 || !command) {
|
|
232
|
+
return /* @__PURE__ */ jsx5("div", { className: `text-xs text-muted-foreground ${className}`, "data-command-inspector": "", children: "Select a command to edit it." });
|
|
233
|
+
}
|
|
234
|
+
const values = command;
|
|
235
|
+
const fields = motionCommandFields(command.type);
|
|
236
|
+
const primitive = motionFlairPrimitive(command);
|
|
237
|
+
const commit = (field, raw) => {
|
|
238
|
+
const edit = motionFieldEdit(shot.id, index, field, raw);
|
|
239
|
+
if ("error" in edit) return setLocal(edit.error);
|
|
240
|
+
setLocal(null);
|
|
241
|
+
controller.apply(edit, `Set ${field.label.toLowerCase()}`);
|
|
242
|
+
};
|
|
243
|
+
const name = (id) => elements.find((e) => e.id === id)?.name ?? id;
|
|
244
|
+
const targets = values.targets ?? [];
|
|
245
|
+
const message = (local && local.revision === state.revision ? local.text : null) ?? error;
|
|
246
|
+
const control = (field) => {
|
|
247
|
+
const v = values[field.name];
|
|
248
|
+
const common = { disabled: readOnly, "aria-label": field.label, title: field.description };
|
|
249
|
+
switch (field.kind) {
|
|
250
|
+
case "targets":
|
|
251
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-1", "data-targets": "", children: [
|
|
252
|
+
targets.map((id) => /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-0.5 rounded bg-muted px-1 text-[11px]", "data-target": id, children: [
|
|
253
|
+
name(id),
|
|
254
|
+
!readOnly && targets.length > 1 && /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": `Remove ${id}`, onClick: () => commit(field, targets.filter((t) => t !== id)), children: "\xD7" })
|
|
255
|
+
] }, id)),
|
|
256
|
+
!readOnly && picked && !targets.includes(picked) && /* @__PURE__ */ jsxs3("button", { type: "button", className: "rounded border border-rose-500 px-1 text-[11px] text-rose-500", onClick: () => commit(field, [...targets, picked]), "data-add-picked": "", children: [
|
|
257
|
+
"+ ",
|
|
258
|
+
name(picked)
|
|
259
|
+
] }),
|
|
260
|
+
!readOnly && /* @__PURE__ */ jsxs3("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-[11px]", "aria-label": "Add target", value: "", onChange: (e) => e.target.value && commit(field, [...targets, e.target.value]), children: [
|
|
261
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "+ target\u2026" }),
|
|
262
|
+
elements.filter((e) => !targets.includes(e.id)).map((e) => /* @__PURE__ */ jsx5("option", { value: e.id, children: e.name ? `${e.name} (${e.id})` : e.id }, e.id))
|
|
263
|
+
] })
|
|
264
|
+
] });
|
|
265
|
+
case "node":
|
|
266
|
+
return /* @__PURE__ */ jsxs3("select", { className: input, ...common, value: typeof v === "string" ? v : "", onChange: (e) => commit(field, e.target.value), children: [
|
|
267
|
+
!field.required && /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }),
|
|
268
|
+
elements.filter((e) => field.name !== "path" || e.type === "path").map((e) => /* @__PURE__ */ jsxs3("option", { value: e.id, children: [
|
|
269
|
+
e.name ? `${e.name} (${e.id})` : e.id,
|
|
270
|
+
e.hidden ? " \xB7 hidden" : ""
|
|
271
|
+
] }, e.id))
|
|
272
|
+
] });
|
|
273
|
+
case "enum":
|
|
274
|
+
return /* @__PURE__ */ jsxs3("select", { className: input, ...common, value: typeof v === "string" ? v : "", onChange: (e) => commit(field, e.target.value), children: [
|
|
275
|
+
!field.required && /* @__PURE__ */ jsx5("option", { value: "", children: "default" }),
|
|
276
|
+
field.options.map((o) => /* @__PURE__ */ jsx5("option", { value: o, children: o }, o))
|
|
277
|
+
] });
|
|
278
|
+
case "boolean":
|
|
279
|
+
return /* @__PURE__ */ jsx5("input", { type: "checkbox", ...common, checked: v === true, onChange: (e) => commit(field, e.target.checked) });
|
|
280
|
+
case "ease":
|
|
281
|
+
return /* @__PURE__ */ jsx5(EasePicker, { value: v, disabled: readOnly, onChange: (ease) => commit(field, ease === void 0 ? "" : ease) });
|
|
282
|
+
case "color":
|
|
283
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
|
|
284
|
+
/* @__PURE__ */ jsx5("input", { type: "color", disabled: readOnly, "aria-label": `${field.label} swatch`, value: typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v) ? v : "#000000", onChange: (e) => commit(field, e.target.value) }),
|
|
285
|
+
/* @__PURE__ */ jsx5(CommitInput, { className: input, ...common, value: typeof v === "string" ? v : "", placeholder: "unchanged", onCommit: (raw) => commit(field, raw) })
|
|
286
|
+
] });
|
|
287
|
+
case "point": {
|
|
288
|
+
const p = v ?? void 0;
|
|
289
|
+
return /* @__PURE__ */ jsx5("div", { className: "flex items-center gap-1", children: ["x", "y"].map((axis) => /* @__PURE__ */ jsx5(
|
|
290
|
+
CommitInput,
|
|
291
|
+
{
|
|
292
|
+
className: input,
|
|
293
|
+
type: "number",
|
|
294
|
+
disabled: readOnly,
|
|
295
|
+
"aria-label": `${field.label} ${axis}`,
|
|
296
|
+
placeholder: axis,
|
|
297
|
+
value: p ? String(p[axis]) : "",
|
|
298
|
+
onCommit: (raw) => commit(field, axis === "x" ? { x: raw, y: p ? String(p.y) : raw ? "0" : "" } : { x: p ? String(p.x) : raw ? "0" : "", y: raw })
|
|
299
|
+
},
|
|
300
|
+
axis
|
|
301
|
+
)) });
|
|
302
|
+
}
|
|
303
|
+
case "scale":
|
|
304
|
+
case "number":
|
|
305
|
+
case "integer":
|
|
306
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
|
|
307
|
+
/* @__PURE__ */ jsx5(
|
|
308
|
+
CommitInput,
|
|
309
|
+
{
|
|
310
|
+
className: input,
|
|
311
|
+
type: "number",
|
|
312
|
+
...common,
|
|
313
|
+
min: field.min,
|
|
314
|
+
max: field.max,
|
|
315
|
+
step: field.kind === "integer" ? 1 : field.unit === "ms" ? 50 : field.unit === "0\u20131" ? 0.05 : "any",
|
|
316
|
+
placeholder: "default",
|
|
317
|
+
value: typeof v === "number" ? String(v) : "",
|
|
318
|
+
onCommit: (raw) => commit(field, raw)
|
|
319
|
+
}
|
|
320
|
+
),
|
|
321
|
+
field.unit && /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-[10px] text-muted-foreground", children: field.unit })
|
|
322
|
+
] });
|
|
323
|
+
default:
|
|
324
|
+
return /* @__PURE__ */ jsx5(CommitInput, { className: input, ...common, value: typeof v === "string" ? v : "", onCommit: (raw) => commit(field, raw) });
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-1.5 text-xs ${className}`, "data-command-inspector": "", "data-command-type": command.type, children: [
|
|
328
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
329
|
+
/* @__PURE__ */ jsxs3("span", { className: "font-medium", children: [
|
|
330
|
+
index + 1,
|
|
331
|
+
". ",
|
|
332
|
+
command.type
|
|
333
|
+
] }),
|
|
334
|
+
primitive ? /* @__PURE__ */ jsx5("code", { className: "rounded bg-muted px-1 text-[10px]", "data-flair-primitive": primitive, title: "The Flair motion primitive this command plays", children: primitive }) : /* @__PURE__ */ jsx5("span", { className: "text-[10px] text-muted-foreground", children: "editor-only (no Flair primitive)" })
|
|
335
|
+
] }),
|
|
336
|
+
["main", "timing"].map((group) => /* @__PURE__ */ jsxs3("fieldset", { className: "grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1", children: [
|
|
337
|
+
/* @__PURE__ */ jsx5("legend", { className: "mb-1 text-[10px] uppercase tracking-wide text-muted-foreground", children: group === "main" ? "Command" : "Timing" }),
|
|
338
|
+
fields.filter((f) => f.group === group).map((field) => /* @__PURE__ */ jsxs3(React3.Fragment, { children: [
|
|
339
|
+
/* @__PURE__ */ jsxs3("label", { className: "truncate text-muted-foreground", title: field.description, children: [
|
|
340
|
+
field.label,
|
|
341
|
+
field.required ? " *" : ""
|
|
342
|
+
] }),
|
|
343
|
+
/* @__PURE__ */ jsx5("div", { "data-field": field.name, children: control(field) })
|
|
344
|
+
] }, field.name))
|
|
345
|
+
] }, group)),
|
|
346
|
+
message && /* @__PURE__ */ jsx5("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
|
|
347
|
+
] });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/animation-editor/CommandList.tsx
|
|
351
|
+
import { useState as useState2 } from "react";
|
|
352
|
+
import {
|
|
353
|
+
MOTION_COMMAND_TYPES,
|
|
354
|
+
defaultMotionCommand,
|
|
355
|
+
duplicateMotionCommandEdit,
|
|
356
|
+
motionCommandRows
|
|
357
|
+
} from "@pixerate/editor/animation-editor";
|
|
358
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
359
|
+
var btn = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
360
|
+
function CommandList({ controller, shot, targets, className = "" }) {
|
|
361
|
+
const state = useAnimationEditorState(controller);
|
|
362
|
+
const { scene, selection, readOnly, error } = state;
|
|
363
|
+
const shotId = shot ?? selection?.shot ?? scene.shots[0]?.id;
|
|
364
|
+
const current = scene.shots.find((s) => s.id === shotId);
|
|
365
|
+
const [type, setType] = useState2("enter");
|
|
366
|
+
const [local, setLocalState] = useState2(null);
|
|
367
|
+
const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
|
|
368
|
+
if (!current) return /* @__PURE__ */ jsx6("div", { className: `text-xs text-muted-foreground ${className}`, "data-command-list": "", children: "Add a shot to start." });
|
|
369
|
+
const rows = motionCommandRows(scene, current.id);
|
|
370
|
+
const selected = selection?.shot === current.id ? selection.command : void 0;
|
|
371
|
+
const run = (edit, label) => {
|
|
372
|
+
setLocal(null);
|
|
373
|
+
return controller.apply(edit, label);
|
|
374
|
+
};
|
|
375
|
+
const add = () => {
|
|
376
|
+
const from = targets?.length ? targets : selected !== void 0 ? current.run[selected].targets ?? [] : [];
|
|
377
|
+
const command = defaultMotionCommand(type, from, scene);
|
|
378
|
+
if ("error" in command) return setLocal(command.error);
|
|
379
|
+
const at = selected !== void 0 ? selected + 1 : current.run.length;
|
|
380
|
+
const result = run({ op: "add_commands", shot: current.id, commands: [command], at }, `Add ${type}`);
|
|
381
|
+
if (result.ok) controller.select({ shot: current.id, command: at });
|
|
382
|
+
};
|
|
383
|
+
const message = (local && local.revision === state.revision ? local.text : null) ?? error;
|
|
384
|
+
return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-1 text-xs ${className}`, "data-command-list": "", "data-for-shot": current.id, children: [
|
|
385
|
+
/* @__PURE__ */ jsxs4("div", { className: "font-medium", children: [
|
|
386
|
+
"Commands in ",
|
|
387
|
+
/* @__PURE__ */ jsx6("span", { className: "text-rose-500", children: current.label ?? current.id })
|
|
388
|
+
] }),
|
|
389
|
+
/* @__PURE__ */ jsx6("ol", { className: "flex flex-col gap-1", "aria-label": "Commands", children: rows.map((row) => {
|
|
390
|
+
const c = row.command;
|
|
391
|
+
const isSelected = row.index === selected;
|
|
392
|
+
return /* @__PURE__ */ jsxs4(
|
|
393
|
+
"li",
|
|
394
|
+
{
|
|
395
|
+
className: `flex flex-wrap items-center gap-1 rounded border px-1.5 py-1 ${isSelected ? "border-rose-500 bg-rose-500/10" : "border-border"}`,
|
|
396
|
+
"data-command": row.index,
|
|
397
|
+
"data-selected": isSelected || void 0,
|
|
398
|
+
onClick: () => controller.select({ shot: current.id, command: row.index }),
|
|
399
|
+
children: [
|
|
400
|
+
/* @__PURE__ */ jsxs4("span", { className: "min-w-0 flex-1 truncate", title: row.summary, children: [
|
|
401
|
+
row.index + 1,
|
|
402
|
+
". ",
|
|
403
|
+
row.summary
|
|
404
|
+
] }),
|
|
405
|
+
row.flairPrimitive && /* @__PURE__ */ jsx6("code", { className: "rounded bg-muted px-1 text-[10px] text-muted-foreground", "data-flair-primitive": row.flairPrimitive, children: row.flairPrimitive }),
|
|
406
|
+
/* @__PURE__ */ jsxs4("span", { className: "tabular-nums text-[10px] text-muted-foreground", children: [
|
|
407
|
+
((row.end - row.start) / 1e3).toFixed(2),
|
|
408
|
+
" s"
|
|
409
|
+
] }),
|
|
410
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex w-full items-center gap-1", onClick: (e) => e.stopPropagation(), children: [
|
|
411
|
+
/* @__PURE__ */ jsxs4(
|
|
412
|
+
"select",
|
|
413
|
+
{
|
|
414
|
+
className: "rounded border border-border bg-transparent px-1 py-0.5 text-[11px]",
|
|
415
|
+
"aria-label": "Starts",
|
|
416
|
+
value: c.with ?? "after",
|
|
417
|
+
disabled: readOnly || row.index === 0,
|
|
418
|
+
onChange: (e) => run(
|
|
419
|
+
e.target.value === "after" ? { op: "update_command", shot: current.id, command: row.index, remove: ["with"] } : { op: "update_command", shot: current.id, command: row.index, set: { with: e.target.value } },
|
|
420
|
+
"Change start"
|
|
421
|
+
),
|
|
422
|
+
children: [
|
|
423
|
+
/* @__PURE__ */ jsx6("option", { value: "after", children: "after previous" }),
|
|
424
|
+
/* @__PURE__ */ jsx6("option", { value: "previous", children: "with previous" })
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
),
|
|
428
|
+
/* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-0.5 text-muted-foreground", children: [
|
|
429
|
+
"+",
|
|
430
|
+
/* @__PURE__ */ jsx6(
|
|
431
|
+
"input",
|
|
432
|
+
{
|
|
433
|
+
className: "w-14 rounded border border-border bg-transparent px-1 py-0.5 text-[11px] tabular-nums",
|
|
434
|
+
type: "number",
|
|
435
|
+
min: 0,
|
|
436
|
+
step: 50,
|
|
437
|
+
"aria-label": "Delay",
|
|
438
|
+
placeholder: "0",
|
|
439
|
+
defaultValue: c.delay ?? "",
|
|
440
|
+
disabled: readOnly,
|
|
441
|
+
onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(),
|
|
442
|
+
onBlur: (e) => {
|
|
443
|
+
const raw = e.currentTarget.value.trim();
|
|
444
|
+
if (raw === String(c.delay ?? "")) return;
|
|
445
|
+
const n = Number(raw);
|
|
446
|
+
if (raw && !(Number.isFinite(n) && n >= 0)) return setLocal("Delay must be a number of ms, 0 or more.");
|
|
447
|
+
run(raw && n > 0 ? { op: "update_command", shot: current.id, command: row.index, set: { delay: n } } : { op: "update_command", shot: current.id, command: row.index, remove: ["delay"] }, "Change delay");
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
String(c.delay ?? "")
|
|
451
|
+
),
|
|
452
|
+
"ms"
|
|
453
|
+
] }),
|
|
454
|
+
/* @__PURE__ */ jsxs4("span", { className: "ml-auto flex gap-0.5", children: [
|
|
455
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn, "aria-label": "Move up", disabled: readOnly || row.index === 0, onClick: () => run({ op: "move_command", shot: current.id, command: row.index, to: row.index - 1 }, "Move command").ok && controller.select({ shot: current.id, command: row.index - 1 }), children: "\u2191" }),
|
|
456
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn, "aria-label": "Move down", disabled: readOnly || row.index === rows.length - 1, onClick: () => run({ op: "move_command", shot: current.id, command: row.index, to: row.index + 1 }, "Move command").ok && controller.select({ shot: current.id, command: row.index + 1 }), children: "\u2193" }),
|
|
457
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn, "aria-label": "Duplicate", disabled: readOnly, onClick: () => run(duplicateMotionCommandEdit(scene, current.id, row.index), "Duplicate command").ok && controller.select({ shot: current.id, command: row.index + 1 }), children: "\u29C9" }),
|
|
458
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn, "aria-label": "Delete", disabled: readOnly, onClick: () => run({ op: "delete_commands", shot: current.id, commands: [row.index] }, "Delete command"), children: "\u2715" })
|
|
459
|
+
] })
|
|
460
|
+
] })
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
`${row.index}:${row.summary}`
|
|
464
|
+
);
|
|
465
|
+
}) }),
|
|
466
|
+
!readOnly && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
|
|
467
|
+
/* @__PURE__ */ jsx6("select", { className: "flex-1 rounded border border-border bg-transparent px-1 py-0.5", "aria-label": "New command type", value: type, onChange: (e) => setType(e.target.value), children: MOTION_COMMAND_TYPES.map((t) => /* @__PURE__ */ jsx6("option", { value: t, children: t }, t)) }),
|
|
468
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn, onClick: add, "data-add-command": "", children: "Add command" })
|
|
469
|
+
] }),
|
|
470
|
+
message && /* @__PURE__ */ jsx6("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
|
|
471
|
+
] });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/animation-editor/PlayheadScrubber.tsx
|
|
475
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
|
|
476
|
+
import { motionBeatTime, motionCueTime, motionRulerTicks } from "@pixerate/editor/animation-editor";
|
|
477
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
478
|
+
var seconds2 = (ms) => `${(ms / 1e3).toFixed(2)} s`;
|
|
479
|
+
function worst2(issues) {
|
|
480
|
+
if (issues.some((i) => i.severity === "error")) return "error";
|
|
481
|
+
if (issues.some((i) => i.severity === "warning")) return "warning";
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
function PlayheadScrubber({ controller, className = "" }) {
|
|
485
|
+
const state = useAnimationEditorState(controller);
|
|
486
|
+
const { duration, shots, time, playing, selection, issues, scene, readOnly } = state;
|
|
487
|
+
const span = Math.max(1, duration);
|
|
488
|
+
const track = useRef2(null);
|
|
489
|
+
const [width, setWidth] = useState3(600);
|
|
490
|
+
useEffect2(() => {
|
|
491
|
+
const el = track.current;
|
|
492
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
493
|
+
const observer = new ResizeObserver(() => setWidth(el.clientWidth || 600));
|
|
494
|
+
observer.observe(el);
|
|
495
|
+
return () => observer.disconnect();
|
|
496
|
+
}, []);
|
|
497
|
+
const ticks = motionRulerTicks(duration, width);
|
|
498
|
+
const pct = (ms) => `${Math.min(span, Math.max(0, ms)) / span * 100}%`;
|
|
499
|
+
const seekAt = (clientX) => {
|
|
500
|
+
const box = track.current.getBoundingClientRect();
|
|
501
|
+
controller.seek(Math.min(1, Math.max(0, (clientX - box.left) / Math.max(1, box.width))) * span);
|
|
502
|
+
};
|
|
503
|
+
const beats = [];
|
|
504
|
+
if (scene.beat && scene.beat.bpm > 0) {
|
|
505
|
+
for (let b = 0, t = motionBeatTime(scene.beat, 0); t <= span && beats.length < 400; t = motionBeatTime(scene.beat, ++b)) if (t >= 0) beats.push(t);
|
|
506
|
+
}
|
|
507
|
+
return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-1 ${className}`, "data-playhead-scrubber": "", "data-shot-timeline": "", children: [
|
|
508
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
509
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "rounded border border-border px-2 py-0.5", onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", disabled: !shots.length, children: playing ? "Pause" : "Play" }),
|
|
510
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "rounded border border-border px-2 py-0.5", onClick: () => {
|
|
511
|
+
controller.pause();
|
|
512
|
+
controller.seek(0);
|
|
513
|
+
}, "aria-label": "Back to start", children: "\u23EE" }),
|
|
514
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-muted-foreground", children: [
|
|
515
|
+
"Loop",
|
|
516
|
+
/* @__PURE__ */ jsxs5(
|
|
517
|
+
"select",
|
|
518
|
+
{
|
|
519
|
+
className: "rounded border border-border bg-transparent px-1 py-0.5",
|
|
520
|
+
value: scene.loop ?? "none",
|
|
521
|
+
disabled: readOnly,
|
|
522
|
+
"aria-label": "Loop",
|
|
523
|
+
onChange: (e) => controller.apply({ op: "set_settings", loop: e.target.value }, "Change loop"),
|
|
524
|
+
children: [
|
|
525
|
+
/* @__PURE__ */ jsx7("option", { value: "none", children: "Off" }),
|
|
526
|
+
/* @__PURE__ */ jsx7("option", { value: "loop", children: "Loop" }),
|
|
527
|
+
/* @__PURE__ */ jsx7("option", { value: "ping-pong", children: "Ping-pong" })
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
] }),
|
|
532
|
+
/* @__PURE__ */ jsxs5("span", { className: "ml-auto tabular-nums text-muted-foreground", "data-time": "", children: [
|
|
533
|
+
seconds2(time),
|
|
534
|
+
" / ",
|
|
535
|
+
seconds2(duration)
|
|
536
|
+
] })
|
|
537
|
+
] }),
|
|
538
|
+
/* @__PURE__ */ jsxs5(
|
|
539
|
+
"div",
|
|
540
|
+
{
|
|
541
|
+
ref: track,
|
|
542
|
+
className: "relative cursor-ew-resize select-none touch-none",
|
|
543
|
+
role: "slider",
|
|
544
|
+
"aria-label": "Playhead",
|
|
545
|
+
"aria-valuemin": 0,
|
|
546
|
+
"aria-valuemax": duration,
|
|
547
|
+
"aria-valuenow": Math.round(time),
|
|
548
|
+
tabIndex: 0,
|
|
549
|
+
"data-scrub-track": "",
|
|
550
|
+
onPointerDown: (e) => {
|
|
551
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
552
|
+
seekAt(e.clientX);
|
|
553
|
+
},
|
|
554
|
+
onPointerMove: (e) => {
|
|
555
|
+
if (e.buttons & 1) seekAt(e.clientX);
|
|
556
|
+
},
|
|
557
|
+
onKeyDown: (e) => {
|
|
558
|
+
const step = e.shiftKey ? 1e3 : 100;
|
|
559
|
+
if (e.key === "ArrowRight") controller.seek(time + step);
|
|
560
|
+
else if (e.key === "ArrowLeft") controller.seek(time - step);
|
|
561
|
+
else if (e.key === "Home") controller.seek(0);
|
|
562
|
+
else if (e.key === "End") controller.seek(duration);
|
|
563
|
+
else if (e.key === " ") controller.toggle();
|
|
564
|
+
else return;
|
|
565
|
+
e.preventDefault();
|
|
566
|
+
},
|
|
567
|
+
children: [
|
|
568
|
+
/* @__PURE__ */ jsxs5("div", { className: "relative h-5 border-b border-border text-[10px] text-muted-foreground", "data-ruler": "", children: [
|
|
569
|
+
beats.map((t, i) => /* @__PURE__ */ jsx7("div", { className: "absolute bottom-0 h-1.5 w-px bg-sky-500/60", style: { left: pct(t) }, "data-beat": i }, `b${i}`)),
|
|
570
|
+
scene.narration?.words.map((w, i) => /* @__PURE__ */ jsx7("div", { className: "absolute top-0 h-1.5 w-px bg-emerald-500/70", style: { left: pct(w.start) }, title: `\u201C${w.text}\u201D at ${seconds2(w.start)}`, "data-word": i }, `w${i}`)),
|
|
571
|
+
ticks.map((t) => /* @__PURE__ */ jsx7("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: pct(t.time) }, children: t.label && /* @__PURE__ */ jsx7("span", { className: "absolute -top-0.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time))
|
|
572
|
+
] }),
|
|
573
|
+
/* @__PURE__ */ jsx7("div", { className: "mt-1 flex w-full gap-px", role: "group", "aria-label": "Shots", children: shots.map((shot, i) => {
|
|
574
|
+
const mark = worst2(issues.filter((x) => x.shot === shot.id));
|
|
575
|
+
const selected = selection?.shot === shot.id;
|
|
576
|
+
const cue = scene.shots[i]?.at;
|
|
577
|
+
const cueTime = cue ? motionCueTime(scene, cue) : void 0;
|
|
578
|
+
return /* @__PURE__ */ jsxs5(
|
|
579
|
+
"button",
|
|
580
|
+
{
|
|
581
|
+
type: "button",
|
|
582
|
+
className: `relative min-w-0 truncate rounded border px-1 py-1 text-left text-[11px] ${selected ? "border-rose-500 bg-rose-500/10" : "border-border bg-muted/40"} ${state.currentShot === shot.id ? "font-semibold" : ""}`,
|
|
583
|
+
style: { flexGrow: Math.max(1, shot.end - shot.start), flexBasis: 0 },
|
|
584
|
+
title: `${shot.label ?? shot.id} \xB7 ${seconds2(shot.start)}\u2013${seconds2(shot.end)}${cueTime !== void 0 ? ` \xB7 cued at ${seconds2(cueTime)}` : ""}`,
|
|
585
|
+
"data-shot": shot.id,
|
|
586
|
+
"data-issue": mark ?? void 0,
|
|
587
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
588
|
+
onClick: () => {
|
|
589
|
+
controller.select({ shot: shot.id });
|
|
590
|
+
controller.seekShot(shot.id);
|
|
591
|
+
},
|
|
592
|
+
children: [
|
|
593
|
+
mark && /* @__PURE__ */ jsx7("span", { className: mark === "error" ? "text-red-500" : "text-amber-500", children: "\u25CF " }),
|
|
594
|
+
shot.label ?? shot.id
|
|
595
|
+
]
|
|
596
|
+
},
|
|
597
|
+
shot.id
|
|
598
|
+
);
|
|
599
|
+
}) }),
|
|
600
|
+
/* @__PURE__ */ jsx7("div", { className: "pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: pct(time) }, "data-playhead": "" })
|
|
601
|
+
]
|
|
602
|
+
}
|
|
603
|
+
)
|
|
151
604
|
] });
|
|
152
605
|
}
|
|
606
|
+
|
|
607
|
+
// src/animation-editor/SceneSettings.tsx
|
|
608
|
+
import { useState as useState4 } from "react";
|
|
609
|
+
import { fitMotionSceneEdits, formatMotionNarration, parseMotionNarration } from "@pixerate/editor/animation-editor";
|
|
610
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
611
|
+
var input2 = "w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs";
|
|
612
|
+
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
613
|
+
var unit = (u) => /* @__PURE__ */ jsx8("span", { className: "shrink-0 text-[10px] text-muted-foreground", children: u });
|
|
614
|
+
function SceneSettings({ controller, className = "" }) {
|
|
615
|
+
const state = useAnimationEditorState(controller);
|
|
616
|
+
const { scene, readOnly, error, duration } = state;
|
|
617
|
+
const [local, setLocalState] = useState4(null);
|
|
618
|
+
const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
|
|
619
|
+
const [fitTo, setFitTo] = useState4("");
|
|
620
|
+
const apply = (edit, label) => {
|
|
621
|
+
setLocal(null);
|
|
622
|
+
return controller.apply(edit, label);
|
|
623
|
+
};
|
|
624
|
+
const settings = (set, label) => apply({ op: "set_settings", ...set }, label);
|
|
625
|
+
const num = (raw, what, ok, then) => {
|
|
626
|
+
if (raw.trim() === "") return then(null);
|
|
627
|
+
const n = Number(raw);
|
|
628
|
+
if (!Number.isFinite(n) || !ok(n)) return setLocal(what);
|
|
629
|
+
then(n);
|
|
630
|
+
};
|
|
631
|
+
const target = scene.target;
|
|
632
|
+
const fitSeconds = fitTo || String(target ? (target[0] + target[1]) / 2 : Math.round(duration / 100) / 10);
|
|
633
|
+
const fit = () => {
|
|
634
|
+
const s = Number(fitSeconds);
|
|
635
|
+
if (!(Number.isFinite(s) && s > 0)) return setLocal("Give the length to fit in seconds, above 0.");
|
|
636
|
+
try {
|
|
637
|
+
const edits = fitMotionSceneEdits(scene, s * 1e3);
|
|
638
|
+
if (edits.length) apply(edits, `Fit to ${s} s`);
|
|
639
|
+
else setLocal(null);
|
|
640
|
+
} catch (e) {
|
|
641
|
+
setLocal(e.message);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
const beat = scene.beat;
|
|
645
|
+
const message = (local && local.revision === state.revision ? local.text : null) ?? error;
|
|
646
|
+
return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-1.5 text-xs ${className}`, "data-scene-settings": "", children: [
|
|
647
|
+
/* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1", children: [
|
|
648
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", children: "Loop" }),
|
|
649
|
+
/* @__PURE__ */ jsxs6("select", { className: input2, "aria-label": "Scene loop", disabled: readOnly, value: scene.loop ?? "none", onChange: (e) => settings({ loop: e.target.value }, "Change loop"), children: [
|
|
650
|
+
/* @__PURE__ */ jsx8("option", { value: "none", children: "Off" }),
|
|
651
|
+
/* @__PURE__ */ jsx8("option", { value: "loop", children: "Loop" }),
|
|
652
|
+
/* @__PURE__ */ jsx8("option", { value: "ping-pong", children: "Ping-pong" })
|
|
653
|
+
] }),
|
|
654
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", children: "Plays on" }),
|
|
655
|
+
/* @__PURE__ */ jsxs6("select", { className: input2, "aria-label": "Scene trigger", title: "How a viewer plays it when exported to Flair", disabled: readOnly, value: scene.trigger ?? "", onChange: (e) => settings({ trigger: e.target.value || null }, "Change trigger"), children: [
|
|
656
|
+
/* @__PURE__ */ jsx8("option", { value: "", children: "Its own (autoplay)" }),
|
|
657
|
+
/* @__PURE__ */ jsx8("option", { value: "click", children: "Click (a shot per click)" }),
|
|
658
|
+
/* @__PURE__ */ jsx8("option", { value: "hover", children: "Hover" }),
|
|
659
|
+
/* @__PURE__ */ jsx8("option", { value: "press", children: "Press" }),
|
|
660
|
+
/* @__PURE__ */ jsx8("option", { value: "scroll", children: "Scroll" })
|
|
661
|
+
] }),
|
|
662
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", children: "Frame rate" }),
|
|
663
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
|
|
664
|
+
/* @__PURE__ */ jsx8(
|
|
665
|
+
CommitInput,
|
|
666
|
+
{
|
|
667
|
+
className: input2,
|
|
668
|
+
type: "number",
|
|
669
|
+
min: 1,
|
|
670
|
+
max: 120,
|
|
671
|
+
"aria-label": "Frame rate",
|
|
672
|
+
placeholder: "30",
|
|
673
|
+
disabled: readOnly,
|
|
674
|
+
value: scene.fps !== void 0 ? String(scene.fps) : "",
|
|
675
|
+
onCommit: (raw) => num(raw, "Frame rate must be 1 to 120 fps.", (n) => n >= 1 && n <= 120, (n) => n !== null && settings({ fps: n }, "Change frame rate"))
|
|
676
|
+
}
|
|
677
|
+
),
|
|
678
|
+
unit("fps")
|
|
679
|
+
] }),
|
|
680
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", title: "Hold changes on a grid of this many frames a second (12 = on twos); empty is smooth", children: "Steps" }),
|
|
681
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
|
|
682
|
+
/* @__PURE__ */ jsx8(
|
|
683
|
+
CommitInput,
|
|
684
|
+
{
|
|
685
|
+
className: input2,
|
|
686
|
+
type: "number",
|
|
687
|
+
min: 1,
|
|
688
|
+
max: 120,
|
|
689
|
+
"aria-label": "Steps",
|
|
690
|
+
placeholder: "smooth",
|
|
691
|
+
disabled: readOnly,
|
|
692
|
+
value: scene.steps !== void 0 ? String(scene.steps) : "",
|
|
693
|
+
onCommit: (raw) => num(raw, "Steps must be above 0 and at most 120 fps, or empty for smooth.", (n) => n > 0 && n <= 120, (n) => settings({ steps: n }, "Change steps"))
|
|
694
|
+
}
|
|
695
|
+
),
|
|
696
|
+
unit("fps")
|
|
697
|
+
] }),
|
|
698
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", children: "Target length" }),
|
|
699
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
|
|
700
|
+
[0, 1].map((i) => /* @__PURE__ */ jsx8(
|
|
701
|
+
CommitInput,
|
|
702
|
+
{
|
|
703
|
+
className: input2,
|
|
704
|
+
type: "number",
|
|
705
|
+
min: 0,
|
|
706
|
+
step: 0.5,
|
|
707
|
+
"aria-label": i ? "Target max" : "Target min",
|
|
708
|
+
placeholder: i ? "max" : "min",
|
|
709
|
+
disabled: readOnly,
|
|
710
|
+
value: target ? String(target[i]) : "",
|
|
711
|
+
onCommit: (raw) => num(raw, "Target lengths are seconds, 0 or more.", (n) => n >= 0, (n) => {
|
|
712
|
+
if (n === null) return settings({ target: null }, "Clear target length");
|
|
713
|
+
const next = target ? [...target] : [n, n];
|
|
714
|
+
next[i] = n;
|
|
715
|
+
if (next[0] > next[1]) return setLocal("The shortest target length must not be longer than the longest.");
|
|
716
|
+
settings({ target: next }, "Change target length");
|
|
717
|
+
})
|
|
718
|
+
},
|
|
719
|
+
i
|
|
720
|
+
)),
|
|
721
|
+
unit("s")
|
|
722
|
+
] }),
|
|
723
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", children: "Fit to" }),
|
|
724
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
|
|
725
|
+
/* @__PURE__ */ jsx8("input", { className: input2, type: "number", min: 0.1, step: 0.5, "aria-label": "Fit to", value: fitSeconds, onChange: (e) => setFitTo(e.target.value) }),
|
|
726
|
+
unit("s"),
|
|
727
|
+
/* @__PURE__ */ jsx8("button", { type: "button", className: btn2, disabled: readOnly || !scene.shots.length, onClick: fit, "data-fit": "", title: "Change the shots' speeds so the scene lasts this long", children: "Fit" })
|
|
728
|
+
] }),
|
|
729
|
+
/* @__PURE__ */ jsx8("label", { className: "text-muted-foreground", children: "Beat grid" }),
|
|
730
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", "data-beat-grid": "", children: [
|
|
731
|
+
/* @__PURE__ */ jsx8(
|
|
732
|
+
CommitInput,
|
|
733
|
+
{
|
|
734
|
+
className: input2,
|
|
735
|
+
type: "number",
|
|
736
|
+
min: 1,
|
|
737
|
+
"aria-label": "BPM",
|
|
738
|
+
placeholder: "bpm",
|
|
739
|
+
disabled: readOnly,
|
|
740
|
+
value: beat ? String(beat.bpm) : "",
|
|
741
|
+
onCommit: (raw) => num(raw, "BPM must be above 0.", (n) => n > 0, (n) => settings({ beat: n === null ? null : { ...beat ?? {}, bpm: n } }, n === null ? "Clear beat grid" : "Change tempo"))
|
|
742
|
+
}
|
|
743
|
+
),
|
|
744
|
+
/* @__PURE__ */ jsx8(
|
|
745
|
+
CommitInput,
|
|
746
|
+
{
|
|
747
|
+
className: input2,
|
|
748
|
+
type: "number",
|
|
749
|
+
"aria-label": "Beat offset",
|
|
750
|
+
placeholder: "offset ms",
|
|
751
|
+
disabled: readOnly || !beat,
|
|
752
|
+
value: beat?.offset !== void 0 ? String(beat.offset) : "",
|
|
753
|
+
onCommit: (raw) => beat && num(raw, "The offset is a number of ms.", () => true, (n) => settings({ beat: { ...beat, offset: n ?? 0 } }, "Change beat offset"))
|
|
754
|
+
}
|
|
755
|
+
),
|
|
756
|
+
/* @__PURE__ */ jsx8(
|
|
757
|
+
CommitInput,
|
|
758
|
+
{
|
|
759
|
+
className: input2,
|
|
760
|
+
type: "number",
|
|
761
|
+
min: 1,
|
|
762
|
+
step: 1,
|
|
763
|
+
"aria-label": "Meter",
|
|
764
|
+
placeholder: "4/bar",
|
|
765
|
+
disabled: readOnly || !beat,
|
|
766
|
+
value: beat?.meter !== void 0 ? String(beat.meter) : "",
|
|
767
|
+
onCommit: (raw) => beat && num(raw, "The meter is a whole number of beats a bar.", (n) => Number.isInteger(n) && n >= 1, (n) => settings({ beat: { ...beat, meter: n ?? 4 } }, "Change meter"))
|
|
768
|
+
}
|
|
769
|
+
)
|
|
770
|
+
] })
|
|
771
|
+
] }),
|
|
772
|
+
/* @__PURE__ */ jsxs6("label", { className: "flex flex-col gap-0.5", children: [
|
|
773
|
+
/* @__PURE__ */ jsx8("span", { className: "text-muted-foreground", children: "Narration (a word a line: start ms, then the word)" }),
|
|
774
|
+
/* @__PURE__ */ jsx8(
|
|
775
|
+
"textarea",
|
|
776
|
+
{
|
|
777
|
+
className: `${input2} h-20 font-mono`,
|
|
778
|
+
"aria-label": "Narration",
|
|
779
|
+
placeholder: "0 Meet\n320 the\n480 badge",
|
|
780
|
+
disabled: readOnly,
|
|
781
|
+
defaultValue: formatMotionNarration(scene.narration),
|
|
782
|
+
onBlur: (e) => {
|
|
783
|
+
if (e.currentTarget.value === formatMotionNarration(scene.narration)) return;
|
|
784
|
+
const parsed = parseMotionNarration(e.currentTarget.value);
|
|
785
|
+
if ("error" in parsed) return setLocal(parsed.error);
|
|
786
|
+
settings({ narration: parsed.narration }, parsed.narration ? "Change narration" : "Clear narration");
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
formatMotionNarration(scene.narration)
|
|
790
|
+
)
|
|
791
|
+
] }),
|
|
792
|
+
message && /* @__PURE__ */ jsx8("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
|
|
793
|
+
] });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/animation-editor/ShotInspector.tsx
|
|
797
|
+
import { useMemo as useMemo5, useState as useState5 } from "react";
|
|
798
|
+
import { describeMotionElements as describeMotionElements2, freshShotId, motionCueChoices, motionCueFromKey, motionCueKey } from "@pixerate/editor/animation-editor";
|
|
799
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
800
|
+
var input3 = "w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs";
|
|
801
|
+
var btn3 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
802
|
+
function ShotInspector({ controller, shot, className = "" }) {
|
|
803
|
+
const state = useAnimationEditorState(controller);
|
|
804
|
+
const { scene, selection, readOnly, error } = state;
|
|
805
|
+
const [local, setLocalState] = useState5(null);
|
|
806
|
+
const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
|
|
807
|
+
const elements = useMemo5(() => describeMotionElements2(scene), [scene.artwork]);
|
|
808
|
+
const cues = useMemo5(() => motionCueChoices(scene), [scene]);
|
|
809
|
+
const id = shot ?? selection?.shot ?? scene.shots[0]?.id;
|
|
810
|
+
const index = scene.shots.findIndex((s) => s.id === id);
|
|
811
|
+
const current = scene.shots[index];
|
|
812
|
+
const apply = (edit, label) => {
|
|
813
|
+
setLocal(null);
|
|
814
|
+
return controller.apply(edit, label);
|
|
815
|
+
};
|
|
816
|
+
const addShot = () => {
|
|
817
|
+
const newId = freshShotId(scene, "shot");
|
|
818
|
+
const at = index >= 0 ? index + 1 : scene.shots.length;
|
|
819
|
+
if (apply({ op: "insert_shots", shots: [{ id: newId, label: "New shot", run: [] }], at }, "Add shot").ok) {
|
|
820
|
+
controller.select({ shot: newId });
|
|
821
|
+
controller.seekShot(newId);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
const number = (key, raw, label) => {
|
|
825
|
+
const n = Number(raw);
|
|
826
|
+
if (raw.trim() === "") return apply({ op: "update_shot", shot: current.id, remove: [key] }, label);
|
|
827
|
+
if (!Number.isFinite(n) || n < 0 || key === "speed" && n <= 0) return setLocal(key === "speed" ? "Speed must be a number above 0 (1 = as written)." : "Hold must be a number of ms, 0 or more.");
|
|
828
|
+
apply({ op: "update_shot", shot: current.id, set: { [key]: n } }, label);
|
|
829
|
+
};
|
|
830
|
+
const message = (local && local.revision === state.revision ? local.text : null) ?? error;
|
|
831
|
+
const cueKey = current ? motionCueKey(current.at) : "";
|
|
832
|
+
const frame = current?.camera && "frame" in current.camera ? current.camera.frame : "";
|
|
833
|
+
return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-1.5 text-xs ${className}`, "data-shot-inspector": "", children: [
|
|
834
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
835
|
+
/* @__PURE__ */ jsx9("span", { className: "font-medium", children: "Shot" }),
|
|
836
|
+
current && /* @__PURE__ */ jsx9("code", { className: "text-[10px] text-muted-foreground", children: current.id }),
|
|
837
|
+
/* @__PURE__ */ jsxs7("span", { className: "ml-auto flex gap-0.5", children: [
|
|
838
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: btn3, disabled: readOnly, onClick: addShot, "data-add-shot": "", children: "+ Shot" }),
|
|
839
|
+
current && /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
840
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: btn3, "aria-label": "Move shot earlier", disabled: readOnly || index === 0, onClick: () => apply({ op: "move_shot", shot: current.id, to: index - 1 }, "Move shot"), children: "\u2190" }),
|
|
841
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: btn3, "aria-label": "Move shot later", disabled: readOnly || index === scene.shots.length - 1, onClick: () => apply({ op: "move_shot", shot: current.id, to: index + 1 }, "Move shot"), children: "\u2192" }),
|
|
842
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: btn3, "aria-label": "Delete shot", disabled: readOnly, onClick: () => apply({ op: "delete_shots", shots: [current.id] }, "Delete shot"), children: "\u2715" })
|
|
843
|
+
] })
|
|
844
|
+
] })
|
|
845
|
+
] }),
|
|
846
|
+
current && /* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1", children: [
|
|
847
|
+
/* @__PURE__ */ jsx9("label", { className: "text-muted-foreground", children: "Label" }),
|
|
848
|
+
/* @__PURE__ */ jsx9(CommitInput, { className: input3, "aria-label": "Shot label", disabled: readOnly, value: current.label ?? "", onCommit: (raw) => apply(raw.trim() ? { op: "update_shot", shot: current.id, set: { label: raw.trim() } } : { op: "update_shot", shot: current.id, remove: ["label"] }, "Rename shot") }),
|
|
849
|
+
/* @__PURE__ */ jsx9("label", { className: "text-muted-foreground", children: "Hold" }),
|
|
850
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
851
|
+
/* @__PURE__ */ jsx9(CommitInput, { className: input3, type: "number", min: 0, step: 50, "aria-label": "Hold", placeholder: "600", disabled: readOnly, value: current.hold !== void 0 ? String(current.hold) : "", onCommit: (raw) => number("hold", raw, "Change hold") }),
|
|
852
|
+
/* @__PURE__ */ jsx9("span", { className: "text-[10px] text-muted-foreground", children: "ms" })
|
|
853
|
+
] }),
|
|
854
|
+
/* @__PURE__ */ jsx9("label", { className: "text-muted-foreground", children: "Speed" }),
|
|
855
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
856
|
+
/* @__PURE__ */ jsx9(CommitInput, { className: input3, type: "number", min: 0.05, step: 0.05, "aria-label": "Speed", placeholder: "1", disabled: readOnly, value: current.speed !== void 0 ? String(current.speed) : "", onCommit: (raw) => number("speed", raw, "Change speed") }),
|
|
857
|
+
/* @__PURE__ */ jsx9("span", { className: "text-[10px] text-muted-foreground", children: "\xD7" })
|
|
858
|
+
] }),
|
|
859
|
+
/* @__PURE__ */ jsx9("label", { className: "text-muted-foreground", children: "Camera" }),
|
|
860
|
+
/* @__PURE__ */ jsxs7(
|
|
861
|
+
"select",
|
|
862
|
+
{
|
|
863
|
+
className: input3,
|
|
864
|
+
"aria-label": "Camera frames",
|
|
865
|
+
disabled: readOnly,
|
|
866
|
+
value: current.camera ? frame ? `frame:${frame}` : "rect" : "",
|
|
867
|
+
onChange: (e) => {
|
|
868
|
+
const v = e.target.value;
|
|
869
|
+
if (!v) apply({ op: "update_shot", shot: current.id, remove: ["camera"] }, "Clear camera");
|
|
870
|
+
else if (v.startsWith("frame:")) apply({ op: "update_shot", shot: current.id, set: { camera: { frame: v.slice(6), padding: 24 } } }, "Frame camera");
|
|
871
|
+
},
|
|
872
|
+
children: [
|
|
873
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: "keep framing" }),
|
|
874
|
+
current.camera && !frame && /* @__PURE__ */ jsx9("option", { value: "rect", children: "a rectangle" }),
|
|
875
|
+
elements.map((e) => /* @__PURE__ */ jsxs7("option", { value: `frame:${e.id}`, children: [
|
|
876
|
+
"frame ",
|
|
877
|
+
e.name ?? e.id
|
|
878
|
+
] }, e.id))
|
|
879
|
+
]
|
|
880
|
+
}
|
|
881
|
+
),
|
|
882
|
+
/* @__PURE__ */ jsx9("label", { className: "text-muted-foreground", children: "Starts on" }),
|
|
883
|
+
/* @__PURE__ */ jsxs7(
|
|
884
|
+
"select",
|
|
885
|
+
{
|
|
886
|
+
className: input3,
|
|
887
|
+
"aria-label": "Cue",
|
|
888
|
+
disabled: readOnly || index === 0,
|
|
889
|
+
value: cueKey,
|
|
890
|
+
onChange: (e) => {
|
|
891
|
+
const cue = motionCueFromKey(e.target.value);
|
|
892
|
+
apply(cue ? { op: "update_shot", shot: current.id, set: { at: cue } } : { op: "update_shot", shot: current.id, remove: ["at"] }, "Cue shot");
|
|
893
|
+
},
|
|
894
|
+
children: [
|
|
895
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: "after the shot before" }),
|
|
896
|
+
cueKey && !cues.some((c) => c.value === cueKey) && /* @__PURE__ */ jsx9("option", { value: cueKey, children: cueKey }),
|
|
897
|
+
cues.map((c) => /* @__PURE__ */ jsx9("option", { value: c.value, children: c.label }, c.value))
|
|
898
|
+
]
|
|
899
|
+
}
|
|
900
|
+
),
|
|
901
|
+
!cues.length && index > 0 && /* @__PURE__ */ jsx9("p", { className: "col-span-2 text-[10px] text-muted-foreground", children: "Add a beat grid or narration in the scene settings to cue shots to them." })
|
|
902
|
+
] }),
|
|
903
|
+
message && /* @__PURE__ */ jsx9("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
|
|
904
|
+
] });
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/animation-editor/AnimationEditor.tsx
|
|
908
|
+
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
909
|
+
var ONION = { before: 3, after: 1 };
|
|
910
|
+
var btn4 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
911
|
+
function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues = true, editable = true, className = "" }) {
|
|
912
|
+
const state = useAnimationEditorState(controller);
|
|
913
|
+
const { issues, scene, selection, readOnly } = state;
|
|
914
|
+
const [picked, setPicked] = useState6(null);
|
|
915
|
+
const [onion, setOnion] = useState6(false);
|
|
916
|
+
const [paths, setPaths] = useState6(false);
|
|
917
|
+
const command = selection?.command !== void 0 ? scene.shots.find((s) => s.id === selection.shot)?.run[selection.command] : void 0;
|
|
918
|
+
const targets = command && "targets" in command ? command.targets : [];
|
|
919
|
+
const nodes = useMemo6(() => indexMotionNodes(scene.artwork.nodes), [scene.artwork]);
|
|
920
|
+
const pickedValid = picked && nodes.has(picked) ? picked : null;
|
|
921
|
+
const onPick = (id, event) => {
|
|
922
|
+
if (event.shiftKey && id && command && "targets" in command && selection?.command !== void 0 && !readOnly) {
|
|
923
|
+
const next = targets.includes(id) ? targets.filter((t) => t !== id) : [...targets, id];
|
|
924
|
+
if (next.length) controller.apply({ op: "update_command", shot: selection.shot, command: selection.command, set: { targets: next } }, next.length > targets.length ? "Add target" : "Remove target");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
setPicked(id);
|
|
928
|
+
};
|
|
929
|
+
const stage = /* @__PURE__ */ jsx10(
|
|
930
|
+
AnimationStage,
|
|
931
|
+
{
|
|
932
|
+
controller,
|
|
933
|
+
values,
|
|
934
|
+
onionSkin: onionSkin ?? (onion ? ONION : void 0),
|
|
935
|
+
motionPaths: motionPaths ?? paths,
|
|
936
|
+
onPick: editable ? onPick : void 0,
|
|
937
|
+
highlight: editable ? [...pickedValid ? [pickedValid] : [], ...targets] : void 0,
|
|
938
|
+
className: "rounded-md border border-border bg-muted/40"
|
|
939
|
+
}
|
|
940
|
+
);
|
|
941
|
+
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx10("ul", { className: "space-y-0.5 text-xs", "aria-label": "Animation issues", children: issues.map((issue, i) => /* @__PURE__ */ jsx10("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", children: motionIssueLine(issue) }, i)) });
|
|
942
|
+
if (!editable)
|
|
943
|
+
return /* @__PURE__ */ jsxs8("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
|
|
944
|
+
stage,
|
|
945
|
+
/* @__PURE__ */ jsx10(PlayheadScrubber, { controller }),
|
|
946
|
+
issueList
|
|
947
|
+
] });
|
|
948
|
+
return /* @__PURE__ */ jsxs8(
|
|
949
|
+
"div",
|
|
950
|
+
{
|
|
951
|
+
className: `grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] ${className}`,
|
|
952
|
+
"data-animation-editor": "",
|
|
953
|
+
onKeyDown: (e) => {
|
|
954
|
+
const el = e.target;
|
|
955
|
+
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== "z" || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) return;
|
|
956
|
+
e.preventDefault();
|
|
957
|
+
if (e.shiftKey) controller.redo();
|
|
958
|
+
else controller.undo();
|
|
959
|
+
},
|
|
960
|
+
children: [
|
|
961
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
962
|
+
stage,
|
|
963
|
+
/* @__PURE__ */ jsx10(PlayheadScrubber, { controller }),
|
|
964
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
|
|
965
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: btn4, onClick: () => controller.undo(), disabled: !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
|
|
966
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: btn4, onClick: () => controller.redo(), disabled: !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" }),
|
|
967
|
+
onionSkin === void 0 && /* @__PURE__ */ jsxs8("label", { className: "flex items-center gap-1", children: [
|
|
968
|
+
/* @__PURE__ */ jsx10("input", { type: "checkbox", checked: onion, onChange: (e) => setOnion(e.target.checked) }),
|
|
969
|
+
" Onion skin"
|
|
970
|
+
] }),
|
|
971
|
+
motionPaths === void 0 && /* @__PURE__ */ jsxs8("label", { className: "flex items-center gap-1", children: [
|
|
972
|
+
/* @__PURE__ */ jsx10("input", { type: "checkbox", checked: paths, onChange: (e) => setPaths(e.target.checked) }),
|
|
973
|
+
" Motion paths"
|
|
974
|
+
] }),
|
|
975
|
+
/* @__PURE__ */ jsx10("span", { className: "ml-auto", "data-picked-element": pickedValid ?? void 0, children: pickedValid ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
976
|
+
"Picked ",
|
|
977
|
+
/* @__PURE__ */ jsx10("code", { className: "text-rose-500", children: pickedValid }),
|
|
978
|
+
" ",
|
|
979
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: "underline", onClick: () => setPicked(null), children: "clear" })
|
|
980
|
+
] }) : "Click the stage to pick an element; shift-click to toggle it as a target." })
|
|
981
|
+
] }),
|
|
982
|
+
issueList
|
|
983
|
+
] }),
|
|
984
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-col gap-3", children: [
|
|
985
|
+
/* @__PURE__ */ jsx10(CommandList, { controller, targets: pickedValid ? [pickedValid] : void 0 }),
|
|
986
|
+
/* @__PURE__ */ jsx10(CommandInspector, { controller, picked: pickedValid, className: "border-t border-border pt-2" }),
|
|
987
|
+
/* @__PURE__ */ jsxs8("details", { open: true, className: "border-t border-border pt-2", children: [
|
|
988
|
+
/* @__PURE__ */ jsx10("summary", { className: "cursor-pointer text-xs font-medium", children: "Shot" }),
|
|
989
|
+
/* @__PURE__ */ jsx10(ShotInspector, { controller, className: "mt-1" })
|
|
990
|
+
] }),
|
|
991
|
+
/* @__PURE__ */ jsxs8("details", { className: "border-t border-border pt-2", children: [
|
|
992
|
+
/* @__PURE__ */ jsx10("summary", { className: "cursor-pointer text-xs font-medium", children: "Scene settings" }),
|
|
993
|
+
/* @__PURE__ */ jsx10(SceneSettings, { controller, className: "mt-1" })
|
|
994
|
+
] })
|
|
995
|
+
] })
|
|
996
|
+
]
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
}
|
|
153
1000
|
export {
|
|
154
1001
|
AnimationEditor,
|
|
155
1002
|
AnimationStage,
|
|
1003
|
+
CommandInspector,
|
|
1004
|
+
CommandList,
|
|
1005
|
+
EasePicker,
|
|
1006
|
+
PlayheadScrubber,
|
|
1007
|
+
SceneSettings,
|
|
1008
|
+
ShotInspector,
|
|
156
1009
|
ShotTimeline,
|
|
157
1010
|
useAnimationEditor,
|
|
158
1011
|
useAnimationEditorController,
|