@pixerate/editor-svelte 0.51.9 → 0.53.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/AnimationEditor.svelte +109 -10
- package/dist/animation-editor/AnimationEditor.svelte.d.ts +11 -1
- package/dist/animation-editor/AnimationEditor.svelte.d.ts.map +1 -1
- package/dist/animation-editor/AnimationStage.svelte +26 -7
- package/dist/animation-editor/AnimationStage.svelte.d.ts +12 -1
- package/dist/animation-editor/AnimationStage.svelte.d.ts.map +1 -1
- package/dist/animation-editor/CommandInspector.svelte +149 -0
- package/dist/animation-editor/CommandInspector.svelte.d.ts +11 -0
- package/dist/animation-editor/CommandInspector.svelte.d.ts.map +1 -0
- package/dist/animation-editor/CommandList.svelte +153 -0
- package/dist/animation-editor/CommandList.svelte.d.ts +13 -0
- package/dist/animation-editor/CommandList.svelte.d.ts.map +1 -0
- package/dist/animation-editor/EasePicker.svelte +49 -0
- package/dist/animation-editor/EasePicker.svelte.d.ts +15 -0
- package/dist/animation-editor/EasePicker.svelte.d.ts.map +1 -0
- package/dist/animation-editor/PlayheadScrubber.svelte +134 -0
- package/dist/animation-editor/PlayheadScrubber.svelte.d.ts +9 -0
- package/dist/animation-editor/PlayheadScrubber.svelte.d.ts.map +1 -0
- package/dist/animation-editor/SceneSettings.svelte +134 -0
- package/dist/animation-editor/SceneSettings.svelte.d.ts +9 -0
- package/dist/animation-editor/SceneSettings.svelte.d.ts.map +1 -0
- package/dist/animation-editor/ShotInspector.svelte +120 -0
- package/dist/animation-editor/ShotInspector.svelte.d.ts +11 -0
- package/dist/animation-editor/ShotInspector.svelte.d.ts.map +1 -0
- package/dist/animation-editor/index.d.ts +18 -0
- package/dist/animation-editor/index.d.ts.map +1 -1
- package/dist/animation-editor/index.js +6 -0
- package/package.json +2 -2
|
@@ -1,30 +1,129 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { motionIssueLine, type AnimationEditorController, type MotionFrameOptions } from '@pixerate/editor/animation-editor';
|
|
2
|
+
import { indexMotionNodes, motionIssueLine, type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin } from '@pixerate/editor/animation-editor';
|
|
3
3
|
import AnimationStage from './AnimationStage.svelte';
|
|
4
|
-
import
|
|
4
|
+
import CommandInspector from './CommandInspector.svelte';
|
|
5
|
+
import CommandList from './CommandList.svelte';
|
|
6
|
+
import PlayheadScrubber from './PlayheadScrubber.svelte';
|
|
7
|
+
import SceneSettings from './SceneSettings.svelte';
|
|
8
|
+
import ShotInspector from './ShotInspector.svelte';
|
|
5
9
|
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
6
10
|
|
|
7
11
|
interface Props {
|
|
8
12
|
controller: AnimationEditorController;
|
|
9
13
|
values?: MotionFrameOptions['values'];
|
|
14
|
+
/** Faded frames either side of the playhead (see `<AnimationStage onionSkin>`). Default: an Onion skin toggle. */
|
|
15
|
+
onionSkin?: MotionOnionSkin;
|
|
16
|
+
/** Draw where nodes travel (see `<AnimationStage motionPaths>`). Default: a Motion paths toggle. */
|
|
17
|
+
motionPaths?: boolean | readonly string[];
|
|
10
18
|
/** List the scene's issues under the timeline (default true). */
|
|
11
19
|
showIssues?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Show the hand-editing panels: the command list and inspector, the shot inspector, scene
|
|
22
|
+
* settings, undo / redo and picking on the stage (default true). False shows the stage,
|
|
23
|
+
* scrubber and issues only.
|
|
24
|
+
*/
|
|
25
|
+
editable?: boolean;
|
|
12
26
|
class?: string;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
|
-
let { controller, values, showIssues = true, class: className = '' }: Props = $props();
|
|
29
|
+
let { controller, values, onionSkin, motionPaths, showIssues = true, editable = true, class: className = '' }: Props = $props();
|
|
16
30
|
const view = animationEditorStateOf(() => controller);
|
|
31
|
+
const editor = $derived(view.current);
|
|
32
|
+
const ONION: MotionOnionSkin = { before: 3, after: 1 };
|
|
33
|
+
const btn = 'rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40';
|
|
34
|
+
let picked: string | null = $state(null);
|
|
35
|
+
let onion = $state(false);
|
|
36
|
+
let paths = $state(false);
|
|
37
|
+
const nodes = $derived(indexMotionNodes(editor.scene.artwork.nodes));
|
|
38
|
+
const pickedValid = $derived(picked && nodes.has(picked) ? picked : null);
|
|
39
|
+
const command = $derived(editor.selection?.command !== undefined ? editor.scene.shots.find((s) => s.id === editor.selection!.shot)?.run[editor.selection.command] : undefined);
|
|
40
|
+
const targets = $derived(command && 'targets' in command ? command.targets : []);
|
|
41
|
+
const highlight = $derived(editable ? [...(pickedValid ? [pickedValid] : []), ...targets] : undefined);
|
|
42
|
+
|
|
43
|
+
function onpick(id: string | null, event: { shiftKey: boolean }) {
|
|
44
|
+
const sel = editor.selection;
|
|
45
|
+
if (event.shiftKey && id && command && 'targets' in command && sel?.command !== undefined && !editor.readOnly) {
|
|
46
|
+
const next = targets.includes(id) ? targets.filter((t) => t !== id) : [...targets, id];
|
|
47
|
+
if (next.length) controller.apply({ op: 'update_command', shot: sel.shot, command: sel.command, set: { targets: next } }, next.length > targets.length ? 'Add target' : 'Remove target');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
picked = id;
|
|
51
|
+
}
|
|
52
|
+
function onkeydown(e: KeyboardEvent) {
|
|
53
|
+
const el = e.target as HTMLElement;
|
|
54
|
+
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'z' || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) return;
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
if (e.shiftKey) controller.redo();
|
|
57
|
+
else controller.undo();
|
|
58
|
+
}
|
|
17
59
|
</script>
|
|
18
60
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<AnimationStage {controller} {values} class="rounded-md border border-border bg-muted/40" />
|
|
22
|
-
<ShotTimeline {controller} />
|
|
23
|
-
{#if showIssues && view.current.issues.length > 0}
|
|
61
|
+
{#snippet issueList()}
|
|
62
|
+
{#if showIssues && editor.issues.length > 0}
|
|
24
63
|
<ul class="space-y-0.5 text-xs" aria-label="Animation issues">
|
|
25
|
-
{#each
|
|
64
|
+
{#each editor.issues as issue, i (i)}
|
|
26
65
|
<li class={issue.severity === 'error' ? 'text-red-500' : issue.severity === 'warning' ? 'text-amber-500' : 'text-muted-foreground'}>{motionIssueLine(issue)}</li>
|
|
27
66
|
{/each}
|
|
28
67
|
</ul>
|
|
29
68
|
{/if}
|
|
30
|
-
|
|
69
|
+
{/snippet}
|
|
70
|
+
|
|
71
|
+
<!--
|
|
72
|
+
A whole animation editor: the stage (click to pick an element, shift-click to toggle it as a
|
|
73
|
+
target of the selected command), the playhead scrubber, and panels to edit commands, shots and
|
|
74
|
+
scene settings by hand. Every edit is one undo step.
|
|
75
|
+
-->
|
|
76
|
+
{#if !editable}
|
|
77
|
+
<div class="flex flex-col gap-2 {className}" data-animation-editor="">
|
|
78
|
+
<AnimationStage {controller} {values} onionSkin={onionSkin} {motionPaths} class="rounded-md border border-border bg-muted/40" />
|
|
79
|
+
<PlayheadScrubber {controller} />
|
|
80
|
+
{@render issueList()}
|
|
81
|
+
</div>
|
|
82
|
+
{:else}
|
|
83
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
84
|
+
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] {className}" data-animation-editor="" {onkeydown}>
|
|
85
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
86
|
+
<AnimationStage
|
|
87
|
+
{controller}
|
|
88
|
+
{values}
|
|
89
|
+
onionSkin={onionSkin ?? (onion ? ONION : undefined)}
|
|
90
|
+
motionPaths={motionPaths ?? paths}
|
|
91
|
+
{onpick}
|
|
92
|
+
{highlight}
|
|
93
|
+
class="rounded-md border border-border bg-muted/40"
|
|
94
|
+
/>
|
|
95
|
+
<PlayheadScrubber {controller} />
|
|
96
|
+
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
97
|
+
<button type="button" class={btn} onclick={() => controller.undo()} disabled={!editor.canUndo} title={editor.canUndo ? `Undo ${editor.undoLabel}` : 'Nothing to undo'} data-undo="">Undo</button>
|
|
98
|
+
<button type="button" class={btn} onclick={() => controller.redo()} disabled={!editor.canRedo} title={editor.canRedo ? `Redo ${editor.redoLabel}` : 'Nothing to redo'} data-redo="">Redo</button>
|
|
99
|
+
{#if onionSkin === undefined}
|
|
100
|
+
<label class="flex items-center gap-1"><input type="checkbox" bind:checked={onion} /> Onion skin</label>
|
|
101
|
+
{/if}
|
|
102
|
+
{#if motionPaths === undefined}
|
|
103
|
+
<label class="flex items-center gap-1"><input type="checkbox" bind:checked={paths} /> Motion paths</label>
|
|
104
|
+
{/if}
|
|
105
|
+
<span class="ml-auto" data-picked-element={pickedValid ?? undefined}>
|
|
106
|
+
{#if pickedValid}
|
|
107
|
+
Picked <code class="text-rose-500">{pickedValid}</code>
|
|
108
|
+
<button type="button" class="underline" onclick={() => (picked = null)}>clear</button>
|
|
109
|
+
{:else}
|
|
110
|
+
Click the stage to pick an element; shift-click to toggle it as a target.
|
|
111
|
+
{/if}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
{@render issueList()}
|
|
115
|
+
</div>
|
|
116
|
+
<div class="flex min-w-0 flex-col gap-3">
|
|
117
|
+
<CommandList {controller} targets={pickedValid ? [pickedValid] : undefined} />
|
|
118
|
+
<CommandInspector {controller} picked={pickedValid} class="border-t border-border pt-2" />
|
|
119
|
+
<details open class="border-t border-border pt-2">
|
|
120
|
+
<summary class="cursor-pointer text-xs font-medium">Shot</summary>
|
|
121
|
+
<ShotInspector {controller} class="mt-1" />
|
|
122
|
+
</details>
|
|
123
|
+
<details class="border-t border-border pt-2">
|
|
124
|
+
<summary class="cursor-pointer text-xs font-medium">Scene settings</summary>
|
|
125
|
+
<SceneSettings {controller} class="mt-1" />
|
|
126
|
+
</details>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
{/if}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { type AnimationEditorController, type MotionFrameOptions } from '@pixerate/editor/animation-editor';
|
|
1
|
+
import { type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin } from '@pixerate/editor/animation-editor';
|
|
2
2
|
interface Props {
|
|
3
3
|
controller: AnimationEditorController;
|
|
4
4
|
values?: MotionFrameOptions['values'];
|
|
5
|
+
/** Faded frames either side of the playhead (see `<AnimationStage onionSkin>`). Default: an Onion skin toggle. */
|
|
6
|
+
onionSkin?: MotionOnionSkin;
|
|
7
|
+
/** Draw where nodes travel (see `<AnimationStage motionPaths>`). Default: a Motion paths toggle. */
|
|
8
|
+
motionPaths?: boolean | readonly string[];
|
|
5
9
|
/** List the scene's issues under the timeline (default true). */
|
|
6
10
|
showIssues?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Show the hand-editing panels: the command list and inspector, the shot inspector, scene
|
|
13
|
+
* settings, undo / redo and picking on the stage (default true). False shows the stage,
|
|
14
|
+
* scrubber and issues only.
|
|
15
|
+
*/
|
|
16
|
+
editable?: boolean;
|
|
7
17
|
class?: string;
|
|
8
18
|
}
|
|
9
19
|
declare const AnimationEditor: import("svelte").Component<Props, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimationEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/AnimationEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"AnimationEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/AnimationEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqC,KAAK,yBAAyB,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAUnK,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACtC,kHAAkH;IAClH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,oGAAoG;IACpG,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;IAC1C,iEAAiE;IACjE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqGH,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
2
|
+
import { highlightMotionSvg, motionNodeAtElement, type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin } from '@pixerate/editor/animation-editor';
|
|
3
3
|
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
controller: AnimationEditorController;
|
|
7
7
|
/** Control values to preview (`{ accent: "#0ea5e9" }`). */
|
|
8
8
|
values?: MotionFrameOptions['values'];
|
|
9
|
+
/** Faded frames either side of the playhead. */
|
|
10
|
+
onionSkin?: MotionOnionSkin;
|
|
11
|
+
/** Draw where nodes travel: true for every node whose place changes, or a list of ids. */
|
|
12
|
+
motionPaths?: boolean | readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Called when the stage is clicked, with the id of the artwork node under the pointer (the
|
|
15
|
+
* innermost one; null for the background, ghosts and paths). Makes the stage pickable.
|
|
16
|
+
*/
|
|
17
|
+
onpick?: (id: string | null, event: MouseEvent) => void;
|
|
18
|
+
/** Node ids to outline (e.g. the picked element and the selected command's targets). */
|
|
19
|
+
highlight?: readonly string[];
|
|
9
20
|
class?: string;
|
|
10
21
|
}
|
|
11
22
|
|
|
12
|
-
let { controller, values, class: className = '' }: Props = $props();
|
|
23
|
+
let { controller, values, onionSkin, motionPaths, onpick, highlight, class: className = '' }: Props = $props();
|
|
13
24
|
const view = animationEditorStateOf(() => controller);
|
|
14
25
|
const svg = $derived(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
highlightMotionSvg(
|
|
27
|
+
controller
|
|
28
|
+
.toSvg(view.current.time, { pretty: false, idPrefix: 'ae-', values, onionSkin, motionPaths })
|
|
29
|
+
.replace(/<svg ([^>]*?)width="[^"]*" height="[^"]*"/, '<svg $1width="100%" height="100%"'),
|
|
30
|
+
highlight ?? []
|
|
31
|
+
)
|
|
18
32
|
);
|
|
33
|
+
function onclick(event: MouseEvent) {
|
|
34
|
+
onpick?.(motionNodeAtElement(view.current.scene, event.target as Element, event.currentTarget as Element), event);
|
|
35
|
+
}
|
|
19
36
|
</script>
|
|
20
37
|
|
|
21
|
-
<!-- The frame at the playhead
|
|
38
|
+
<!-- The frame at the playhead as inline SVG (keeping node ids, so a click can pick an element). -->
|
|
39
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
22
40
|
<div
|
|
23
|
-
class="relative overflow-hidden {className}"
|
|
41
|
+
class="relative overflow-hidden {onpick ? 'cursor-crosshair' : ''} {className}"
|
|
24
42
|
style="aspect-ratio:{view.current.scene.artwork.width} / {view.current.scene.artwork.height}"
|
|
25
43
|
data-animation-stage=""
|
|
26
44
|
role="img"
|
|
27
45
|
aria-label="{view.current.scene.name} at {(view.current.time / 1000).toFixed(2)} s"
|
|
46
|
+
onclick={onpick ? onclick : undefined}
|
|
28
47
|
>
|
|
29
48
|
{@html svg}
|
|
30
49
|
</div>
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnimationEditorController, type MotionFrameOptions, type MotionOnionSkin } from '@pixerate/editor/animation-editor';
|
|
2
2
|
interface Props {
|
|
3
3
|
controller: AnimationEditorController;
|
|
4
4
|
/** Control values to preview (`{ accent: "#0ea5e9" }`). */
|
|
5
5
|
values?: MotionFrameOptions['values'];
|
|
6
|
+
/** Faded frames either side of the playhead. */
|
|
7
|
+
onionSkin?: MotionOnionSkin;
|
|
8
|
+
/** Draw where nodes travel: true for every node whose place changes, or a list of ids. */
|
|
9
|
+
motionPaths?: boolean | readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* Called when the stage is clicked, with the id of the artwork node under the pointer (the
|
|
12
|
+
* innermost one; null for the background, ghosts and paths). Makes the stage pickable.
|
|
13
|
+
*/
|
|
14
|
+
onpick?: (id: string | null, event: MouseEvent) => void;
|
|
15
|
+
/** Node ids to outline (e.g. the picked element and the selected command's targets). */
|
|
16
|
+
highlight?: readonly string[];
|
|
6
17
|
class?: string;
|
|
7
18
|
}
|
|
8
19
|
declare const AnimationStage: import("svelte").Component<Props, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimationStage.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/AnimationStage.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"AnimationStage.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/AnimationStage.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAA2C,KAAK,yBAAyB,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAIzK,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACtC,gDAAgD;IAChD,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,0FAA0F;IAC1F,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;IAC1C;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACxD,wFAAwF;IACxF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6BH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
describeMotionElements,
|
|
4
|
+
motionCommandFields,
|
|
5
|
+
motionFieldEdit,
|
|
6
|
+
motionFlairPrimitive,
|
|
7
|
+
type AnimationEditorController,
|
|
8
|
+
type MotionFieldSpec,
|
|
9
|
+
} from '@pixerate/editor/animation-editor';
|
|
10
|
+
import EasePicker from './EasePicker.svelte';
|
|
11
|
+
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
controller: AnimationEditorController;
|
|
15
|
+
/** An element picked on the stage, offered as a target to add. */
|
|
16
|
+
picked?: string | null;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { controller, picked = null, class: className = '' }: Props = $props();
|
|
21
|
+
const view = animationEditorStateOf(() => controller);
|
|
22
|
+
const editor = $derived(view.current);
|
|
23
|
+
const elements = $derived(describeMotionElements(editor.scene));
|
|
24
|
+
const shot = $derived(editor.selection ? editor.scene.shots.find((s) => s.id === editor.selection!.shot) : undefined);
|
|
25
|
+
const index = $derived(editor.selection?.command);
|
|
26
|
+
const command = $derived(shot && index !== undefined ? shot.run[index] : undefined);
|
|
27
|
+
const values = $derived((command ?? {}) as unknown as Record<string, unknown>);
|
|
28
|
+
const fields = $derived(command ? motionCommandFields(command.type) : []);
|
|
29
|
+
const primitive = $derived(command ? motionFlairPrimitive(command) : undefined);
|
|
30
|
+
const targets = $derived((values.targets as string[] | undefined) ?? []);
|
|
31
|
+
let localError = $state<{ text: string; revision: number } | null>(null);
|
|
32
|
+
// A local error shows until the editor changes; then the editor's own error (if any) shows.
|
|
33
|
+
const message = $derived((localError && localError.revision === editor.revision ? localError.text : null) ?? editor.error);
|
|
34
|
+
function setLocal(text: string | null) {
|
|
35
|
+
localError = text ? { text, revision: editor.revision } : null;
|
|
36
|
+
}
|
|
37
|
+
const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs';
|
|
38
|
+
|
|
39
|
+
function commit(field: MotionFieldSpec, raw: Parameters<typeof motionFieldEdit>[3]) {
|
|
40
|
+
if (!shot || index === undefined) return;
|
|
41
|
+
const edit = motionFieldEdit(shot.id, index, field, raw);
|
|
42
|
+
if ('error' in edit) {
|
|
43
|
+
setLocal(edit.error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setLocal(null);
|
|
47
|
+
controller.apply(edit, `Set ${field.label.toLowerCase()}`);
|
|
48
|
+
}
|
|
49
|
+
const name = (id: string) => elements.find((e) => e.id === id)?.name ?? id;
|
|
50
|
+
const str = (v: unknown) => (typeof v === 'string' ? v : '');
|
|
51
|
+
const numStr = (v: unknown) => (typeof v === 'number' ? String(v) : '');
|
|
52
|
+
const point = (v: unknown) => v as { x: number; y: number } | undefined;
|
|
53
|
+
const step = (f: MotionFieldSpec) => (f.kind === 'integer' ? 1 : f.unit === 'ms' ? 50 : f.unit === '0–1' ? 0.05 : 'any');
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<!--
|
|
57
|
+
A form for the selected command, generated from MOTION_COMMAND_SCHEMA (the schema the agent
|
|
58
|
+
tools take): enums as selects, numbers with units and limits, node pickers and an ease picker.
|
|
59
|
+
-->
|
|
60
|
+
{#if !shot || index === undefined || !command}
|
|
61
|
+
<div class="text-xs text-muted-foreground {className}" data-command-inspector="">Select a command to edit it.</div>
|
|
62
|
+
{:else}
|
|
63
|
+
<div class="flex flex-col gap-1.5 text-xs {className}" data-command-inspector="" data-command-type={command.type}>
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<span class="font-medium">{index + 1}. {command.type}</span>
|
|
66
|
+
{#if primitive}
|
|
67
|
+
<code class="rounded bg-muted px-1 text-[10px]" data-flair-primitive={primitive} title="The Flair motion primitive this command plays">{primitive}</code>
|
|
68
|
+
{:else}
|
|
69
|
+
<span class="text-[10px] text-muted-foreground">editor-only (no Flair primitive)</span>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
72
|
+
{#each ['main', 'timing'] as group (group)}
|
|
73
|
+
<fieldset class="grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1">
|
|
74
|
+
<legend class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">{group === 'main' ? 'Command' : 'Timing'}</legend>
|
|
75
|
+
{#each fields.filter((f) => f.group === group) as field (field.name)}
|
|
76
|
+
{@const v = values[field.name]}
|
|
77
|
+
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
78
|
+
<label class="truncate text-muted-foreground" title={field.description}>{field.label}{field.required ? ' *' : ''}</label>
|
|
79
|
+
<div data-field={field.name}>
|
|
80
|
+
{#if field.kind === 'targets'}
|
|
81
|
+
<div class="flex flex-wrap items-center gap-1" data-targets="">
|
|
82
|
+
{#each targets as id (id)}
|
|
83
|
+
<span class="flex items-center gap-0.5 rounded bg-muted px-1 text-[11px]" data-target={id}>
|
|
84
|
+
{name(id)}
|
|
85
|
+
{#if !editor.readOnly && targets.length > 1}
|
|
86
|
+
<button type="button" aria-label="Remove {id}" onclick={() => commit(field, targets.filter((t) => t !== id))}>×</button>
|
|
87
|
+
{/if}
|
|
88
|
+
</span>
|
|
89
|
+
{/each}
|
|
90
|
+
{#if !editor.readOnly && picked && !targets.includes(picked)}
|
|
91
|
+
<button type="button" class="rounded border border-rose-500 px-1 text-[11px] text-rose-500" onclick={() => commit(field, [...targets, picked!])} data-add-picked="">+ {name(picked)}</button>
|
|
92
|
+
{/if}
|
|
93
|
+
{#if !editor.readOnly}
|
|
94
|
+
<select class="rounded border border-border bg-transparent px-1 py-0.5 text-[11px]" aria-label="Add target" value="" onchange={(e) => e.currentTarget.value && commit(field, [...targets, e.currentTarget.value])}>
|
|
95
|
+
<option value="">+ target…</option>
|
|
96
|
+
{#each elements.filter((e) => !targets.includes(e.id)) as e (e.id)}
|
|
97
|
+
<option value={e.id}>{e.name ? `${e.name} (${e.id})` : e.id}</option>
|
|
98
|
+
{/each}
|
|
99
|
+
</select>
|
|
100
|
+
{/if}
|
|
101
|
+
</div>
|
|
102
|
+
{:else if field.kind === 'node'}
|
|
103
|
+
<select class={input} aria-label={field.label} title={field.description} disabled={editor.readOnly} value={str(v)} onchange={(e) => commit(field, e.currentTarget.value)}>
|
|
104
|
+
{#if !field.required}<option value="">—</option>{/if}
|
|
105
|
+
{#each elements.filter((e) => field.name !== 'path' || e.type === 'path') as e (e.id)}
|
|
106
|
+
<option value={e.id}>{e.name ? `${e.name} (${e.id})` : e.id}{e.hidden ? ' · hidden' : ''}</option>
|
|
107
|
+
{/each}
|
|
108
|
+
</select>
|
|
109
|
+
{:else if field.kind === 'enum'}
|
|
110
|
+
<select class={input} aria-label={field.label} title={field.description} disabled={editor.readOnly} value={str(v)} onchange={(e) => commit(field, e.currentTarget.value)}>
|
|
111
|
+
{#if !field.required}<option value="">default</option>{/if}
|
|
112
|
+
{#each field.options ?? [] as o (o)}
|
|
113
|
+
<option value={o}>{o}</option>
|
|
114
|
+
{/each}
|
|
115
|
+
</select>
|
|
116
|
+
{:else if field.kind === 'boolean'}
|
|
117
|
+
<input type="checkbox" aria-label={field.label} title={field.description} disabled={editor.readOnly} checked={v === true} onchange={(e) => commit(field, e.currentTarget.checked)} />
|
|
118
|
+
{:else if field.kind === 'ease'}
|
|
119
|
+
<EasePicker value={v as never} disabled={editor.readOnly} onchange={(ease) => commit(field, ease === undefined ? '' : (ease as string))} />
|
|
120
|
+
{:else if field.kind === 'color'}
|
|
121
|
+
<div class="flex items-center gap-1">
|
|
122
|
+
<input type="color" disabled={editor.readOnly} aria-label="{field.label} swatch" value={/^#[0-9a-f]{6}$/i.test(str(v)) ? str(v) : '#000000'} onchange={(e) => commit(field, e.currentTarget.value)} />
|
|
123
|
+
<input class={input} aria-label={field.label} title={field.description} disabled={editor.readOnly} placeholder="unchanged" value={str(v)} onchange={(e) => commit(field, e.currentTarget.value)} />
|
|
124
|
+
</div>
|
|
125
|
+
{:else if field.kind === 'point'}
|
|
126
|
+
{@const p = point(v)}
|
|
127
|
+
<div class="flex items-center gap-1">
|
|
128
|
+
<input class={input} type="number" disabled={editor.readOnly} aria-label="{field.label} x" placeholder="x" value={p ? String(p.x) : ''}
|
|
129
|
+
onchange={(e) => commit(field, { x: e.currentTarget.value, y: p ? String(p.y) : e.currentTarget.value ? '0' : '' })} />
|
|
130
|
+
<input class={input} type="number" disabled={editor.readOnly} aria-label="{field.label} y" placeholder="y" value={p ? String(p.y) : ''}
|
|
131
|
+
onchange={(e) => commit(field, { x: p ? String(p.x) : e.currentTarget.value ? '0' : '', y: e.currentTarget.value })} />
|
|
132
|
+
</div>
|
|
133
|
+
{:else if field.kind === 'number' || field.kind === 'integer' || field.kind === 'scale'}
|
|
134
|
+
<div class="flex items-center gap-1">
|
|
135
|
+
<input class={input} type="number" aria-label={field.label} title={field.description} disabled={editor.readOnly} min={field.min} max={field.max} step={step(field)} placeholder="default" value={numStr(v)} onchange={(e) => commit(field, e.currentTarget.value)} />
|
|
136
|
+
{#if field.unit}<span class="shrink-0 text-[10px] text-muted-foreground">{field.unit}</span>{/if}
|
|
137
|
+
</div>
|
|
138
|
+
{:else}
|
|
139
|
+
<input class={input} aria-label={field.label} title={field.description} disabled={editor.readOnly} value={str(v)} onchange={(e) => commit(field, e.currentTarget.value)} />
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
{/each}
|
|
143
|
+
</fieldset>
|
|
144
|
+
{/each}
|
|
145
|
+
{#if message}
|
|
146
|
+
<p class="text-red-500" role="alert" data-edit-error="">{message}</p>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AnimationEditorController } from '@pixerate/editor/animation-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: AnimationEditorController;
|
|
4
|
+
/** An element picked on the stage, offered as a target to add. */
|
|
5
|
+
picked?: string | null;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const CommandInspector: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type CommandInspector = ReturnType<typeof CommandInspector>;
|
|
10
|
+
export default CommandInspector;
|
|
11
|
+
//# sourceMappingURL=CommandInspector.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/CommandInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAKH,KAAK,yBAAyB,EAE/B,MAAM,mCAAmC,CAAC;AAK3C,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuIH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
MOTION_COMMAND_TYPES,
|
|
4
|
+
defaultMotionCommand,
|
|
5
|
+
duplicateMotionCommandEdit,
|
|
6
|
+
motionCommandRows,
|
|
7
|
+
type AnimationEditorController,
|
|
8
|
+
type MotionCommandType,
|
|
9
|
+
type MotionEdit,
|
|
10
|
+
} from '@pixerate/editor/animation-editor';
|
|
11
|
+
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
controller: AnimationEditorController;
|
|
15
|
+
/** The shot to list (default: the selected shot, else the first). */
|
|
16
|
+
shot?: string;
|
|
17
|
+
/** Targets for a new command (e.g. the element picked on the stage); default: the selected command's. */
|
|
18
|
+
targets?: readonly string[];
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { controller, shot, targets, class: className = '' }: Props = $props();
|
|
23
|
+
const view = animationEditorStateOf(() => controller);
|
|
24
|
+
const editor = $derived(view.current);
|
|
25
|
+
const shotId = $derived(shot ?? editor.selection?.shot ?? editor.scene.shots[0]?.id);
|
|
26
|
+
const current = $derived(editor.scene.shots.find((s) => s.id === shotId));
|
|
27
|
+
const rows = $derived(current ? motionCommandRows(editor.scene, current.id) : []);
|
|
28
|
+
const selected = $derived(current && editor.selection?.shot === current.id ? editor.selection.command : undefined);
|
|
29
|
+
let type: MotionCommandType = $state('enter');
|
|
30
|
+
let localError = $state<{ text: string; revision: number } | null>(null);
|
|
31
|
+
// A local error shows until the editor changes; then the editor's own error (if any) shows.
|
|
32
|
+
const message = $derived((localError && localError.revision === editor.revision ? localError.text : null) ?? editor.error);
|
|
33
|
+
function setLocal(text: string | null) {
|
|
34
|
+
localError = text ? { text, revision: editor.revision } : null;
|
|
35
|
+
}
|
|
36
|
+
const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
|
|
37
|
+
|
|
38
|
+
function run(edit: MotionEdit | MotionEdit[], label: string) {
|
|
39
|
+
setLocal(null);
|
|
40
|
+
return controller.apply(edit, label);
|
|
41
|
+
}
|
|
42
|
+
function add() {
|
|
43
|
+
if (!current) return;
|
|
44
|
+
const from = targets?.length ? targets : selected !== undefined ? ((current.run[selected] as { targets?: string[] }).targets ?? []) : [];
|
|
45
|
+
const command = defaultMotionCommand(type, from, editor.scene);
|
|
46
|
+
if ('error' in command) {
|
|
47
|
+
setLocal(command.error);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const at = selected !== undefined ? selected + 1 : current.run.length;
|
|
51
|
+
if (run({ op: 'add_commands', shot: current.id, commands: [command], at }, `Add ${type}`).ok) controller.select({ shot: current.id, command: at });
|
|
52
|
+
}
|
|
53
|
+
function move(index: number, to: number) {
|
|
54
|
+
if (current && run({ op: 'move_command', shot: current.id, command: index, to }, 'Move command').ok) controller.select({ shot: current.id, command: to });
|
|
55
|
+
}
|
|
56
|
+
function duplicate(index: number) {
|
|
57
|
+
if (current && run(duplicateMotionCommandEdit(editor.scene, current.id, index), 'Duplicate command').ok) controller.select({ shot: current.id, command: index + 1 });
|
|
58
|
+
}
|
|
59
|
+
function setDelay(index: number, raw: string, before: number | undefined) {
|
|
60
|
+
if (!current) return;
|
|
61
|
+
const text = raw.trim();
|
|
62
|
+
if (text === String(before ?? '')) return;
|
|
63
|
+
const n = Number(text);
|
|
64
|
+
if (text && !(Number.isFinite(n) && n >= 0)) {
|
|
65
|
+
setLocal('Delay must be a number of ms, 0 or more.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
run(text && n > 0 ? { op: 'update_command', shot: current.id, command: index, set: { delay: n } } : { op: 'update_command', shot: current.id, command: index, remove: ['delay'] }, 'Change delay');
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<!--
|
|
73
|
+
A shot's commands in order: select one to inspect it, change `with` and `delay` inline, move,
|
|
74
|
+
duplicate or delete it, and add new ones. Each change is one controller.apply edit (one undo step).
|
|
75
|
+
-->
|
|
76
|
+
{#if !current}
|
|
77
|
+
<div class="text-xs text-muted-foreground {className}" data-command-list="">Add a shot to start.</div>
|
|
78
|
+
{:else}
|
|
79
|
+
<div class="flex flex-col gap-1 text-xs {className}" data-command-list="" data-for-shot={current.id}>
|
|
80
|
+
<div class="font-medium">Commands in <span class="text-rose-500">{current.label ?? current.id}</span></div>
|
|
81
|
+
<ol class="flex flex-col gap-1" aria-label="Commands">
|
|
82
|
+
{#each rows as row (`${row.index}:${row.summary}`)}
|
|
83
|
+
{@const c = row.command as { with?: string; delay?: number }}
|
|
84
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
85
|
+
<li
|
|
86
|
+
class="flex flex-wrap items-center gap-1 rounded border px-1.5 py-1 {row.index === selected ? 'border-rose-500 bg-rose-500/10' : 'border-border'}"
|
|
87
|
+
data-command={row.index}
|
|
88
|
+
data-selected={row.index === selected || undefined}
|
|
89
|
+
onclick={() => controller.select({ shot: current.id, command: row.index })}
|
|
90
|
+
>
|
|
91
|
+
<span class="min-w-0 flex-1 truncate" title={row.summary}>{row.index + 1}. {row.summary}</span>
|
|
92
|
+
{#if row.flairPrimitive}
|
|
93
|
+
<code class="rounded bg-muted px-1 text-[10px] text-muted-foreground" data-flair-primitive={row.flairPrimitive}>{row.flairPrimitive}</code>
|
|
94
|
+
{/if}
|
|
95
|
+
<span class="tabular-nums text-[10px] text-muted-foreground">{((row.end - row.start) / 1000).toFixed(2)} s</span>
|
|
96
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
97
|
+
<div class="flex w-full items-center gap-1" onclick={(e) => e.stopPropagation()}>
|
|
98
|
+
<select
|
|
99
|
+
class="rounded border border-border bg-transparent px-1 py-0.5 text-[11px]"
|
|
100
|
+
aria-label="Starts"
|
|
101
|
+
value={c.with ?? 'after'}
|
|
102
|
+
disabled={editor.readOnly || row.index === 0}
|
|
103
|
+
onchange={(e) =>
|
|
104
|
+
run(
|
|
105
|
+
e.currentTarget.value === 'after'
|
|
106
|
+
? { op: 'update_command', shot: current.id, command: row.index, remove: ['with'] }
|
|
107
|
+
: { op: 'update_command', shot: current.id, command: row.index, set: { with: e.currentTarget.value } },
|
|
108
|
+
'Change start'
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<option value="after">after previous</option>
|
|
112
|
+
<option value="previous">with previous</option>
|
|
113
|
+
</select>
|
|
114
|
+
<label class="flex items-center gap-0.5 text-muted-foreground">
|
|
115
|
+
+
|
|
116
|
+
<input
|
|
117
|
+
class="w-14 rounded border border-border bg-transparent px-1 py-0.5 text-[11px] tabular-nums"
|
|
118
|
+
type="number"
|
|
119
|
+
min="0"
|
|
120
|
+
step="50"
|
|
121
|
+
aria-label="Delay"
|
|
122
|
+
placeholder="0"
|
|
123
|
+
value={c.delay ?? ''}
|
|
124
|
+
disabled={editor.readOnly}
|
|
125
|
+
onchange={(e) => setDelay(row.index, e.currentTarget.value, c.delay)}
|
|
126
|
+
/>
|
|
127
|
+
ms
|
|
128
|
+
</label>
|
|
129
|
+
<span class="ml-auto flex gap-0.5">
|
|
130
|
+
<button type="button" class={btn} aria-label="Move up" disabled={editor.readOnly || row.index === 0} onclick={() => move(row.index, row.index - 1)}>↑</button>
|
|
131
|
+
<button type="button" class={btn} aria-label="Move down" disabled={editor.readOnly || row.index === rows.length - 1} onclick={() => move(row.index, row.index + 1)}>↓</button>
|
|
132
|
+
<button type="button" class={btn} aria-label="Duplicate" disabled={editor.readOnly} onclick={() => duplicate(row.index)}>⧉</button>
|
|
133
|
+
<button type="button" class={btn} aria-label="Delete" disabled={editor.readOnly} onclick={() => run({ op: 'delete_commands', shot: current.id, commands: [row.index] }, 'Delete command')}>✕</button>
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
</li>
|
|
137
|
+
{/each}
|
|
138
|
+
</ol>
|
|
139
|
+
{#if !editor.readOnly}
|
|
140
|
+
<div class="flex items-center gap-1">
|
|
141
|
+
<select class="flex-1 rounded border border-border bg-transparent px-1 py-0.5" aria-label="New command type" bind:value={type}>
|
|
142
|
+
{#each MOTION_COMMAND_TYPES as t (t)}
|
|
143
|
+
<option value={t}>{t}</option>
|
|
144
|
+
{/each}
|
|
145
|
+
</select>
|
|
146
|
+
<button type="button" class={btn} onclick={add} data-add-command="">Add command</button>
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
{#if message}
|
|
150
|
+
<p class="text-red-500" role="alert" data-edit-error="">{message}</p>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
153
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AnimationEditorController } from '@pixerate/editor/animation-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: AnimationEditorController;
|
|
4
|
+
/** The shot to list (default: the selected shot, else the first). */
|
|
5
|
+
shot?: string;
|
|
6
|
+
/** Targets for a new command (e.g. the element picked on the stage); default: the selected command's. */
|
|
7
|
+
targets?: readonly string[];
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const CommandList: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type CommandList = ReturnType<typeof CommandList>;
|
|
12
|
+
export default CommandList;
|
|
13
|
+
//# sourceMappingURL=CommandList.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandList.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/CommandList.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAKH,KAAK,yBAAyB,EAG/B,MAAM,mCAAmC,CAAC;AAI3C,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yGAAyG;IACzG,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqHH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { MOTION_EASE_CHOICES, motionEaseKey, motionEasingCurve, motionEasingGuides, motionEasingPath, type MotionCommandBase } from '@pixerate/editor/animation-editor';
|
|
3
|
+
|
|
4
|
+
/** An easing (a preset name or a curve), as commands take it. */
|
|
5
|
+
type AnimationEasing = NonNullable<MotionCommandBase['ease']>;
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** The easing (undefined: the command type's default). */
|
|
9
|
+
value: AnimationEasing | undefined;
|
|
10
|
+
/** Called with the picked preset, or undefined for the default. */
|
|
11
|
+
onchange: (ease: AnimationEasing | undefined) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { value, onchange, disabled = false, class: className = '' }: Props = $props();
|
|
17
|
+
const W = 72;
|
|
18
|
+
const H = 44;
|
|
19
|
+
const key = $derived(motionEaseKey(value));
|
|
20
|
+
const curve = $derived(motionEasingCurve(value ?? 'ease-out'));
|
|
21
|
+
const guides = $derived(motionEasingGuides(curve, H));
|
|
22
|
+
const spring = $derived(typeof value === 'string' ? value.startsWith('spring') : value?.type === 'spring');
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<!-- Picks an easing preset (springs included) and previews its curve, sampled with evaluateEasing. -->
|
|
26
|
+
<div class="flex items-center gap-2 {className}" data-ease-picker="">
|
|
27
|
+
<select
|
|
28
|
+
class="min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs"
|
|
29
|
+
aria-label="Ease"
|
|
30
|
+
value={key}
|
|
31
|
+
{disabled}
|
|
32
|
+
onchange={(e) => {
|
|
33
|
+
const v = e.currentTarget.value;
|
|
34
|
+
if (v === 'custom') return;
|
|
35
|
+
onchange(v ? (v as AnimationEasing) : undefined);
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{#each MOTION_EASE_CHOICES as c (c.value)}
|
|
39
|
+
<option value={c.value}>{c.label}</option>
|
|
40
|
+
{/each}
|
|
41
|
+
{#if key === 'custom'}<option value="custom">Custom</option>{/if}
|
|
42
|
+
</select>
|
|
43
|
+
<svg width={W} height={H} viewBox="0 0 {W} {H}" class="shrink-0 rounded border border-border" data-ease-curve={key || 'default'} aria-hidden="true">
|
|
44
|
+
<line x1={0} x2={W} y1={guides.zero} y2={guides.zero} stroke="currentColor" stroke-opacity={0.2} />
|
|
45
|
+
<line x1={0} x2={W} y1={guides.one} y2={guides.one} stroke="currentColor" stroke-opacity={0.2} stroke-dasharray="2 2" />
|
|
46
|
+
<path d={motionEasingPath(curve, W, H)} fill="none" stroke="#f43f5e" stroke-width={1.5} />
|
|
47
|
+
</svg>
|
|
48
|
+
{#if spring}<span class="text-[10px] tabular-nums text-muted-foreground">{Math.round(curve.durationMs)} ms</span>{/if}
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MotionCommandBase } from '@pixerate/editor/animation-editor';
|
|
2
|
+
/** An easing (a preset name or a curve), as commands take it. */
|
|
3
|
+
type AnimationEasing = NonNullable<MotionCommandBase['ease']>;
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The easing (undefined: the command type's default). */
|
|
6
|
+
value: AnimationEasing | undefined;
|
|
7
|
+
/** Called with the picked preset, or undefined for the default. */
|
|
8
|
+
onchange: (ease: AnimationEasing | undefined) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const EasePicker: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type EasePicker = ReturnType<typeof EasePicker>;
|
|
14
|
+
export default EasePicker;
|
|
15
|
+
//# sourceMappingURL=EasePicker.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EasePicker.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/EasePicker.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+F,KAAK,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAGtK,iEAAiE;AACjE,KAAK,eAAe,GAAG,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;AAE9D,UAAU,KAAK;IACb,0DAA0D;IAC1D,KAAK,EAAE,eAAe,GAAG,SAAS,CAAC;IACnC,mEAAmE;IACnE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,KAAK,IAAI,CAAC;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { motionBeatTime, motionCueTime, motionRulerTicks, type AnimationEditorController, type MotionIssue } from '@pixerate/editor/animation-editor';
|
|
3
|
+
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
controller: AnimationEditorController;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { controller, class: className = '' }: Props = $props();
|
|
11
|
+
const view = animationEditorStateOf(() => controller);
|
|
12
|
+
const editor = $derived(view.current);
|
|
13
|
+
const span = $derived(Math.max(1, editor.duration));
|
|
14
|
+
let width = $state(600);
|
|
15
|
+
let track: HTMLDivElement | undefined = $state();
|
|
16
|
+
// Not bind:clientWidth: that needs ResizeObserver, which test DOMs (jsdom) lack.
|
|
17
|
+
$effect(() => {
|
|
18
|
+
const el = track;
|
|
19
|
+
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
20
|
+
const observer = new ResizeObserver(() => (width = el.clientWidth || 600));
|
|
21
|
+
observer.observe(el);
|
|
22
|
+
return () => observer.disconnect();
|
|
23
|
+
});
|
|
24
|
+
const ticks = $derived(motionRulerTicks(editor.duration, width || 600));
|
|
25
|
+
const beats = $derived.by(() => {
|
|
26
|
+
const out: number[] = [];
|
|
27
|
+
const grid = editor.scene.beat;
|
|
28
|
+
if (grid && grid.bpm > 0) for (let b = 0, t = motionBeatTime(grid, 0); t <= span && out.length < 400; t = motionBeatTime(grid, ++b)) if (t >= 0) out.push(t);
|
|
29
|
+
return out;
|
|
30
|
+
});
|
|
31
|
+
const seconds = (ms: number) => `${(ms / 1000).toFixed(2)} s`;
|
|
32
|
+
const pct = (ms: number) => `${(Math.min(span, Math.max(0, ms)) / span) * 100}%`;
|
|
33
|
+
const worst = (issues: readonly MotionIssue[]): 'error' | 'warning' | null =>
|
|
34
|
+
issues.some((i) => i.severity === 'error') ? 'error' : issues.some((i) => i.severity === 'warning') ? 'warning' : null;
|
|
35
|
+
|
|
36
|
+
function seekAt(clientX: number) {
|
|
37
|
+
const box = track!.getBoundingClientRect();
|
|
38
|
+
controller.seek(Math.min(1, Math.max(0, (clientX - box.left) / Math.max(1, box.width))) * span);
|
|
39
|
+
}
|
|
40
|
+
function onKey(event: KeyboardEvent) {
|
|
41
|
+
const step = event.shiftKey ? 1000 : 100;
|
|
42
|
+
if (event.key === 'ArrowRight') controller.seek(editor.time + step);
|
|
43
|
+
else if (event.key === 'ArrowLeft') controller.seek(editor.time - step);
|
|
44
|
+
else if (event.key === 'Home') controller.seek(0);
|
|
45
|
+
else if (event.key === 'End') controller.seek(editor.duration);
|
|
46
|
+
else if (event.key === ' ') controller.toggle();
|
|
47
|
+
else return;
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<!--
|
|
53
|
+
Play, pause and loop, a time ruler (with the beat grid and narration words when the scene has
|
|
54
|
+
them) to scrub by dragging, and one band per shot: click a band to select the shot and jump to it.
|
|
55
|
+
-->
|
|
56
|
+
<div class="flex flex-col gap-1 {className}" data-playhead-scrubber="" data-shot-timeline="">
|
|
57
|
+
<div class="flex items-center gap-2 text-xs">
|
|
58
|
+
<button type="button" class="rounded border border-border px-2 py-0.5" onclick={() => controller.toggle()} aria-label={editor.playing ? 'Pause' : 'Play'} disabled={!editor.shots.length}>
|
|
59
|
+
{editor.playing ? 'Pause' : 'Play'}
|
|
60
|
+
</button>
|
|
61
|
+
<button type="button" class="rounded border border-border px-2 py-0.5" onclick={() => { controller.pause(); controller.seek(0); }} aria-label="Back to start">⏮</button>
|
|
62
|
+
<label class="flex items-center gap-1 text-muted-foreground">
|
|
63
|
+
Loop
|
|
64
|
+
<select
|
|
65
|
+
class="rounded border border-border bg-transparent px-1 py-0.5"
|
|
66
|
+
value={editor.scene.loop ?? 'none'}
|
|
67
|
+
disabled={editor.readOnly}
|
|
68
|
+
aria-label="Loop"
|
|
69
|
+
onchange={(e) => controller.apply({ op: 'set_settings', loop: e.currentTarget.value as 'none' | 'loop' | 'ping-pong' }, 'Change loop')}
|
|
70
|
+
>
|
|
71
|
+
<option value="none">Off</option>
|
|
72
|
+
<option value="loop">Loop</option>
|
|
73
|
+
<option value="ping-pong">Ping-pong</option>
|
|
74
|
+
</select>
|
|
75
|
+
</label>
|
|
76
|
+
<span class="ml-auto tabular-nums text-muted-foreground" data-time="">{seconds(editor.time)} / {seconds(editor.duration)}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div
|
|
79
|
+
bind:this={track}
|
|
80
|
+
class="relative cursor-ew-resize select-none touch-none"
|
|
81
|
+
role="slider"
|
|
82
|
+
aria-label="Playhead"
|
|
83
|
+
aria-valuemin={0}
|
|
84
|
+
aria-valuemax={editor.duration}
|
|
85
|
+
aria-valuenow={Math.round(editor.time)}
|
|
86
|
+
tabindex="0"
|
|
87
|
+
data-scrub-track=""
|
|
88
|
+
onpointerdown={(e) => {
|
|
89
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
90
|
+
seekAt(e.clientX);
|
|
91
|
+
}}
|
|
92
|
+
onpointermove={(e) => {
|
|
93
|
+
if (e.buttons & 1) seekAt(e.clientX);
|
|
94
|
+
}}
|
|
95
|
+
onkeydown={onKey}
|
|
96
|
+
>
|
|
97
|
+
<div class="relative h-5 border-b border-border text-[10px] text-muted-foreground" data-ruler="">
|
|
98
|
+
{#each beats as t, i (i)}
|
|
99
|
+
<div class="absolute bottom-0 h-1.5 w-px bg-sky-500/60" style="left:{pct(t)}" data-beat={i}></div>
|
|
100
|
+
{/each}
|
|
101
|
+
{#each editor.scene.narration?.words ?? [] as w, i (i)}
|
|
102
|
+
<div class="absolute top-0 h-1.5 w-px bg-emerald-500/70" style="left:{pct(w.start)}" title="“{w.text}” at {seconds(w.start)}" data-word={i}></div>
|
|
103
|
+
{/each}
|
|
104
|
+
{#each ticks as t (t.time)}
|
|
105
|
+
<div class="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)}">
|
|
106
|
+
{#if t.label}<span class="absolute -top-0.5 left-1 whitespace-nowrap tabular-nums">{t.label}</span>{/if}
|
|
107
|
+
</div>
|
|
108
|
+
{/each}
|
|
109
|
+
</div>
|
|
110
|
+
<div class="mt-1 flex w-full gap-px" role="group" aria-label="Shots">
|
|
111
|
+
{#each editor.shots as shot, i (shot.id)}
|
|
112
|
+
{@const mark = worst(editor.issues.filter((x) => x.shot === shot.id))}
|
|
113
|
+
{@const cue = editor.scene.shots[i]?.at}
|
|
114
|
+
{@const cueTime = cue ? motionCueTime(editor.scene, cue) : undefined}
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="relative min-w-0 truncate rounded border px-1 py-1 text-left text-[11px] {editor.selection?.shot === shot.id ? 'border-rose-500 bg-rose-500/10' : 'border-border bg-muted/40'} {editor.currentShot === shot.id ? 'font-semibold' : ''}"
|
|
118
|
+
style="flex-grow:{Math.max(1, shot.end - shot.start)};flex-basis:0"
|
|
119
|
+
title="{shot.label ?? shot.id} · {seconds(shot.start)}–{seconds(shot.end)}{cueTime !== undefined ? ` · cued at ${seconds(cueTime)}` : ''}"
|
|
120
|
+
data-shot={shot.id}
|
|
121
|
+
data-issue={mark ?? undefined}
|
|
122
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
123
|
+
onclick={() => {
|
|
124
|
+
controller.select({ shot: shot.id });
|
|
125
|
+
controller.seekShot(shot.id);
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{#if mark}<span class={mark === 'error' ? 'text-red-500' : 'text-amber-500'}>● </span>{/if}{shot.label ?? shot.id}
|
|
129
|
+
</button>
|
|
130
|
+
{/each}
|
|
131
|
+
</div>
|
|
132
|
+
<div class="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-rose-500" style="left:{pct(editor.time)}" data-playhead=""></div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AnimationEditorController } from '@pixerate/editor/animation-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: AnimationEditorController;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const PlayheadScrubber: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type PlayheadScrubber = ReturnType<typeof PlayheadScrubber>;
|
|
8
|
+
export default PlayheadScrubber;
|
|
9
|
+
//# sourceMappingURL=PlayheadScrubber.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlayheadScrubber.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/PlayheadScrubber.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAmD,KAAK,yBAAyB,EAAoB,MAAM,mCAAmC,CAAC;AAIpJ,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuGH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fitMotionSceneEdits, formatMotionNarration, parseMotionNarration, type AnimationEditorController, type MotionEdit } from '@pixerate/editor/animation-editor';
|
|
3
|
+
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
controller: AnimationEditorController;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Settings = Omit<Extract<MotionEdit, { op: 'set_settings' }>, 'op'>;
|
|
11
|
+
|
|
12
|
+
let { controller, class: className = '' }: Props = $props();
|
|
13
|
+
const view = animationEditorStateOf(() => controller);
|
|
14
|
+
const editor = $derived(view.current);
|
|
15
|
+
const scene = $derived(editor.scene);
|
|
16
|
+
const beat = $derived(scene.beat);
|
|
17
|
+
const target = $derived(scene.target);
|
|
18
|
+
const narration = $derived(formatMotionNarration(scene.narration));
|
|
19
|
+
let localError = $state<{ text: string; revision: number } | null>(null);
|
|
20
|
+
let fitTo = $state('');
|
|
21
|
+
const fitSeconds = $derived(fitTo || String(target ? (target[0] + target[1]) / 2 : Math.round(editor.duration / 100) / 10));
|
|
22
|
+
// A local error shows until the editor changes; then the editor's own error (if any) shows.
|
|
23
|
+
const message = $derived((localError && localError.revision === editor.revision ? localError.text : null) ?? editor.error);
|
|
24
|
+
function setLocal(text: string | null) {
|
|
25
|
+
localError = text ? { text, revision: editor.revision } : null;
|
|
26
|
+
}
|
|
27
|
+
const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs';
|
|
28
|
+
const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
|
|
29
|
+
|
|
30
|
+
function apply(edit: MotionEdit | MotionEdit[], label: string) {
|
|
31
|
+
setLocal(null);
|
|
32
|
+
return controller.apply(edit, label);
|
|
33
|
+
}
|
|
34
|
+
const settings = (set: Settings, label: string) => apply({ op: 'set_settings', ...set }, label);
|
|
35
|
+
function num(raw: string, what: string, ok: (n: number) => boolean, then: (n: number | null) => void) {
|
|
36
|
+
if (raw.trim() === '') return then(null);
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
if (!Number.isFinite(n) || !ok(n)) {
|
|
39
|
+
setLocal(what);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
then(n);
|
|
43
|
+
}
|
|
44
|
+
function setTarget(i: 0 | 1, raw: string) {
|
|
45
|
+
num(raw, 'Target lengths are seconds, 0 or more.', (n) => n >= 0, (n) => {
|
|
46
|
+
if (n === null) return settings({ target: null }, 'Clear target length');
|
|
47
|
+
const next: [number, number] = target ? [target[0], target[1]] : [n, n];
|
|
48
|
+
next[i] = n;
|
|
49
|
+
if (next[0] > next[1]) {
|
|
50
|
+
setLocal('The shortest target length must not be longer than the longest.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
settings({ target: next }, 'Change target length');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function fit() {
|
|
57
|
+
const s = Number(fitSeconds);
|
|
58
|
+
if (!(Number.isFinite(s) && s > 0)) {
|
|
59
|
+
setLocal('Give the length to fit in seconds, above 0.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const edits = fitMotionSceneEdits(scene, s * 1000);
|
|
64
|
+
if (edits.length) apply(edits, `Fit to ${s} s`);
|
|
65
|
+
else setLocal(null);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
setLocal((e as Error).message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function setNarration(text: string) {
|
|
71
|
+
if (text === narration) return;
|
|
72
|
+
const parsed = parseMotionNarration(text);
|
|
73
|
+
if ('error' in parsed) {
|
|
74
|
+
setLocal(parsed.error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
settings({ narration: parsed.narration }, parsed.narration ? 'Change narration' : 'Clear narration');
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<!--
|
|
82
|
+
Scene settings: loop, frame rate, stepped time, the wanted length with a Fit button
|
|
83
|
+
(fitMotionScene, as fit_animation does), a beat grid and narration words to cue shots to.
|
|
84
|
+
-->
|
|
85
|
+
<div class="flex flex-col gap-1.5 text-xs {className}" data-scene-settings="">
|
|
86
|
+
<div class="grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1">
|
|
87
|
+
<span class="text-muted-foreground">Loop</span>
|
|
88
|
+
<select class={input} aria-label="Scene loop" disabled={editor.readOnly} value={scene.loop ?? 'none'} onchange={(e) => settings({ loop: e.currentTarget.value as 'none' }, 'Change loop')}>
|
|
89
|
+
<option value="none">Off</option>
|
|
90
|
+
<option value="loop">Loop</option>
|
|
91
|
+
<option value="ping-pong">Ping-pong</option>
|
|
92
|
+
</select>
|
|
93
|
+
<span class="text-muted-foreground">Frame rate</span>
|
|
94
|
+
<div class="flex items-center gap-1">
|
|
95
|
+
<input class={input} type="number" min="1" max="120" aria-label="Frame rate" placeholder="30" disabled={editor.readOnly} value={scene.fps ?? ''}
|
|
96
|
+
onchange={(e) => num(e.currentTarget.value, 'Frame rate must be 1 to 120 fps.', (n) => n >= 1 && n <= 120, (n) => n !== null && settings({ fps: n }, 'Change frame rate'))} />
|
|
97
|
+
<span class="shrink-0 text-[10px] text-muted-foreground">fps</span>
|
|
98
|
+
</div>
|
|
99
|
+
<span class="text-muted-foreground" title="Hold changes on a grid of this many frames a second (12 = on twos); empty is smooth">Steps</span>
|
|
100
|
+
<div class="flex items-center gap-1">
|
|
101
|
+
<input class={input} type="number" min="1" max="120" aria-label="Steps" placeholder="smooth" disabled={editor.readOnly} value={scene.steps ?? ''}
|
|
102
|
+
onchange={(e) => num(e.currentTarget.value, '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'))} />
|
|
103
|
+
<span class="shrink-0 text-[10px] text-muted-foreground">fps</span>
|
|
104
|
+
</div>
|
|
105
|
+
<span class="text-muted-foreground">Target length</span>
|
|
106
|
+
<div class="flex items-center gap-1">
|
|
107
|
+
<input class={input} type="number" min="0" step="0.5" aria-label="Target min" placeholder="min" disabled={editor.readOnly} value={target ? target[0] : ''} onchange={(e) => setTarget(0, e.currentTarget.value)} />
|
|
108
|
+
<input class={input} type="number" min="0" step="0.5" aria-label="Target max" placeholder="max" disabled={editor.readOnly} value={target ? target[1] : ''} onchange={(e) => setTarget(1, e.currentTarget.value)} />
|
|
109
|
+
<span class="shrink-0 text-[10px] text-muted-foreground">s</span>
|
|
110
|
+
</div>
|
|
111
|
+
<span class="text-muted-foreground">Fit to</span>
|
|
112
|
+
<div class="flex items-center gap-1">
|
|
113
|
+
<input class={input} type="number" min="0.1" step="0.5" aria-label="Fit to" value={fitSeconds} oninput={(e) => (fitTo = e.currentTarget.value)} />
|
|
114
|
+
<span class="shrink-0 text-[10px] text-muted-foreground">s</span>
|
|
115
|
+
<button type="button" class={btn} disabled={editor.readOnly || !scene.shots.length} onclick={fit} data-fit="" title="Change the shots' speeds so the scene lasts this long">Fit</button>
|
|
116
|
+
</div>
|
|
117
|
+
<span class="text-muted-foreground">Beat grid</span>
|
|
118
|
+
<div class="flex items-center gap-1" data-beat-grid="">
|
|
119
|
+
<input class={input} type="number" min="1" aria-label="BPM" placeholder="bpm" disabled={editor.readOnly} value={beat ? beat.bpm : ''}
|
|
120
|
+
onchange={(e) => num(e.currentTarget.value, 'BPM must be above 0.', (n) => n > 0, (n) => settings({ beat: n === null ? null : { ...(beat ?? {}), bpm: n } }, n === null ? 'Clear beat grid' : 'Change tempo'))} />
|
|
121
|
+
<input class={input} type="number" aria-label="Beat offset" placeholder="offset ms" disabled={editor.readOnly || !beat} value={beat?.offset ?? ''}
|
|
122
|
+
onchange={(e) => beat && num(e.currentTarget.value, 'The offset is a number of ms.', () => true, (n) => settings({ beat: { ...beat, offset: n ?? 0 } }, 'Change beat offset'))} />
|
|
123
|
+
<input class={input} type="number" min="1" step="1" aria-label="Meter" placeholder="4/bar" disabled={editor.readOnly || !beat} value={beat?.meter ?? ''}
|
|
124
|
+
onchange={(e) => beat && num(e.currentTarget.value, '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'))} />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<label class="flex flex-col gap-0.5">
|
|
128
|
+
<span class="text-muted-foreground">Narration (a word a line: start ms, then the word)</span>
|
|
129
|
+
<textarea class="{input} h-20 font-mono" aria-label="Narration" placeholder={'0 Meet\n320 the\n480 badge'} disabled={editor.readOnly} value={narration} onchange={(e) => setNarration(e.currentTarget.value)}></textarea>
|
|
130
|
+
</label>
|
|
131
|
+
{#if message}
|
|
132
|
+
<p class="text-red-500" role="alert" data-edit-error="">{message}</p>
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AnimationEditorController } from '@pixerate/editor/animation-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: AnimationEditorController;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const SceneSettings: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type SceneSettings = ReturnType<typeof SceneSettings>;
|
|
8
|
+
export default SceneSettings;
|
|
9
|
+
//# sourceMappingURL=SceneSettings.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SceneSettings.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/SceneSettings.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoE,KAAK,yBAAyB,EAAmB,MAAM,mCAAmC,CAAC;AAIpK,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+HH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { describeMotionElements, freshShotId, motionCueChoices, motionCueFromKey, motionCueKey, type AnimationEditorController, type MotionEdit } from '@pixerate/editor/animation-editor';
|
|
3
|
+
import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
controller: AnimationEditorController;
|
|
7
|
+
/** The shot to edit (default: the selected shot, else the first). */
|
|
8
|
+
shot?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { controller, shot, class: className = '' }: Props = $props();
|
|
13
|
+
const view = animationEditorStateOf(() => controller);
|
|
14
|
+
const editor = $derived(view.current);
|
|
15
|
+
const elements = $derived(describeMotionElements(editor.scene));
|
|
16
|
+
const cues = $derived(motionCueChoices(editor.scene));
|
|
17
|
+
const id = $derived(shot ?? editor.selection?.shot ?? editor.scene.shots[0]?.id);
|
|
18
|
+
const index = $derived(editor.scene.shots.findIndex((s) => s.id === id));
|
|
19
|
+
const current = $derived(editor.scene.shots[index]);
|
|
20
|
+
const cueKey = $derived(current ? motionCueKey(current.at) : '');
|
|
21
|
+
const frame = $derived(current?.camera && 'frame' in current.camera ? current.camera.frame : '');
|
|
22
|
+
let localError = $state<{ text: string; revision: number } | null>(null);
|
|
23
|
+
// A local error shows until the editor changes; then the editor's own error (if any) shows.
|
|
24
|
+
const message = $derived((localError && localError.revision === editor.revision ? localError.text : null) ?? editor.error);
|
|
25
|
+
function setLocal(text: string | null) {
|
|
26
|
+
localError = text ? { text, revision: editor.revision } : null;
|
|
27
|
+
}
|
|
28
|
+
const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs';
|
|
29
|
+
const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
|
|
30
|
+
|
|
31
|
+
function apply(edit: MotionEdit | MotionEdit[], label: string) {
|
|
32
|
+
setLocal(null);
|
|
33
|
+
return controller.apply(edit, label);
|
|
34
|
+
}
|
|
35
|
+
function addShot() {
|
|
36
|
+
const newId = freshShotId(editor.scene, 'shot');
|
|
37
|
+
const at = index >= 0 ? index + 1 : editor.scene.shots.length;
|
|
38
|
+
if (apply({ op: 'insert_shots', shots: [{ id: newId, label: 'New shot', run: [] }], at }, 'Add shot').ok) {
|
|
39
|
+
controller.select({ shot: newId });
|
|
40
|
+
controller.seekShot(newId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function number(key: 'hold' | 'speed', raw: string, label: string) {
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
if (raw.trim() === '') return apply({ op: 'update_shot', shot: current.id, remove: [key] }, label);
|
|
46
|
+
if (!Number.isFinite(n) || n < 0 || (key === 'speed' && n <= 0)) {
|
|
47
|
+
setLocal(key === 'speed' ? 'Speed must be a number above 0 (1 = as written).' : 'Hold must be a number of ms, 0 or more.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
apply({ op: 'update_shot', shot: current.id, set: { [key]: n } }, label);
|
|
51
|
+
}
|
|
52
|
+
function rename(raw: string) {
|
|
53
|
+
apply(raw.trim() ? { op: 'update_shot', shot: current.id, set: { label: raw.trim() } } : { op: 'update_shot', shot: current.id, remove: ['label'] }, 'Rename shot');
|
|
54
|
+
}
|
|
55
|
+
function setCamera(v: string) {
|
|
56
|
+
if (!v) apply({ op: 'update_shot', shot: current.id, remove: ['camera'] }, 'Clear camera');
|
|
57
|
+
else if (v.startsWith('frame:')) apply({ op: 'update_shot', shot: current.id, set: { camera: { frame: v.slice(6), padding: 24 } } }, 'Frame camera');
|
|
58
|
+
}
|
|
59
|
+
function setCue(v: string) {
|
|
60
|
+
const cue = motionCueFromKey(v);
|
|
61
|
+
apply(cue ? { op: 'update_shot', shot: current.id, set: { at: cue } } : { op: 'update_shot', shot: current.id, remove: ['at'] }, 'Cue shot');
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<!--
|
|
66
|
+
The selected shot: label, hold, speed, the node the camera frames and the cue it starts on (a
|
|
67
|
+
beat or a narration word). Also adds, moves and deletes shots. Every change is one undo step.
|
|
68
|
+
-->
|
|
69
|
+
<div class="flex flex-col gap-1.5 text-xs {className}" data-shot-inspector="">
|
|
70
|
+
<div class="flex items-center gap-1">
|
|
71
|
+
<span class="font-medium">Shot</span>
|
|
72
|
+
{#if current}<code class="text-[10px] text-muted-foreground">{current.id}</code>{/if}
|
|
73
|
+
<span class="ml-auto flex gap-0.5">
|
|
74
|
+
<button type="button" class={btn} disabled={editor.readOnly} onclick={addShot} data-add-shot="">+ Shot</button>
|
|
75
|
+
{#if current}
|
|
76
|
+
<button type="button" class={btn} aria-label="Move shot earlier" disabled={editor.readOnly || index === 0} onclick={() => apply({ op: 'move_shot', shot: current.id, to: index - 1 }, 'Move shot')}>←</button>
|
|
77
|
+
<button type="button" class={btn} aria-label="Move shot later" disabled={editor.readOnly || index === editor.scene.shots.length - 1} onclick={() => apply({ op: 'move_shot', shot: current.id, to: index + 1 }, 'Move shot')}>→</button>
|
|
78
|
+
<button type="button" class={btn} aria-label="Delete shot" disabled={editor.readOnly} onclick={() => apply({ op: 'delete_shots', shots: [current.id] }, 'Delete shot')}>✕</button>
|
|
79
|
+
{/if}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
{#if current}
|
|
83
|
+
<div class="grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1">
|
|
84
|
+
<span class="text-muted-foreground">Label</span>
|
|
85
|
+
<input class={input} aria-label="Shot label" disabled={editor.readOnly} value={current.label ?? ''} onchange={(e) => rename(e.currentTarget.value)} />
|
|
86
|
+
<span class="text-muted-foreground">Hold</span>
|
|
87
|
+
<div class="flex items-center gap-1">
|
|
88
|
+
<input class={input} type="number" min="0" step="50" aria-label="Hold" placeholder="600" disabled={editor.readOnly} value={current.hold ?? ''} onchange={(e) => number('hold', e.currentTarget.value, 'Change hold')} />
|
|
89
|
+
<span class="text-[10px] text-muted-foreground">ms</span>
|
|
90
|
+
</div>
|
|
91
|
+
<span class="text-muted-foreground">Speed</span>
|
|
92
|
+
<div class="flex items-center gap-1">
|
|
93
|
+
<input class={input} type="number" min="0.05" step="0.05" aria-label="Speed" placeholder="1" disabled={editor.readOnly} value={current.speed ?? ''} onchange={(e) => number('speed', e.currentTarget.value, 'Change speed')} />
|
|
94
|
+
<span class="text-[10px] text-muted-foreground">×</span>
|
|
95
|
+
</div>
|
|
96
|
+
<span class="text-muted-foreground">Camera</span>
|
|
97
|
+
<select class={input} aria-label="Camera frames" disabled={editor.readOnly} value={current.camera ? (frame ? `frame:${frame}` : 'rect') : ''} onchange={(e) => setCamera(e.currentTarget.value)}>
|
|
98
|
+
<option value="">keep framing</option>
|
|
99
|
+
{#if current.camera && !frame}<option value="rect">a rectangle</option>{/if}
|
|
100
|
+
{#each elements as e (e.id)}
|
|
101
|
+
<option value="frame:{e.id}">frame {e.name ?? e.id}</option>
|
|
102
|
+
{/each}
|
|
103
|
+
</select>
|
|
104
|
+
<span class="text-muted-foreground">Starts on</span>
|
|
105
|
+
<select class={input} aria-label="Cue" disabled={editor.readOnly || index === 0} value={cueKey} onchange={(e) => setCue(e.currentTarget.value)}>
|
|
106
|
+
<option value="">after the shot before</option>
|
|
107
|
+
{#if cueKey && !cues.some((c) => c.value === cueKey)}<option value={cueKey}>{cueKey}</option>{/if}
|
|
108
|
+
{#each cues as c (c.value)}
|
|
109
|
+
<option value={c.value}>{c.label}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</select>
|
|
112
|
+
{#if !cues.length && index > 0}
|
|
113
|
+
<p class="col-span-2 text-[10px] text-muted-foreground">Add a beat grid or narration in the scene settings to cue shots to them.</p>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
{#if message}
|
|
118
|
+
<p class="text-red-500" role="alert" data-edit-error="">{message}</p>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AnimationEditorController } from '@pixerate/editor/animation-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: AnimationEditorController;
|
|
4
|
+
/** The shot to edit (default: the selected shot, else the first). */
|
|
5
|
+
shot?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ShotInspector: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ShotInspector = ReturnType<typeof ShotInspector>;
|
|
10
|
+
export default ShotInspector;
|
|
11
|
+
//# sourceMappingURL=ShotInspector.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShotInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/animation-editor/ShotInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyF,KAAK,yBAAyB,EAAmB,MAAM,mCAAmC,CAAC;AAIzL,UAAU,KAAK;IACb,UAAU,EAAE,yBAAyB,CAAC;IACtC,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmHH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -2,11 +2,29 @@ import type { ComponentProps } from "svelte";
|
|
|
2
2
|
import type AnimationEditorComponent from "./AnimationEditor.svelte";
|
|
3
3
|
import type AnimationStageComponent from "./AnimationStage.svelte";
|
|
4
4
|
import type ShotTimelineComponent from "./ShotTimeline.svelte";
|
|
5
|
+
import type PlayheadScrubberComponent from "./PlayheadScrubber.svelte";
|
|
6
|
+
import type CommandListComponent from "./CommandList.svelte";
|
|
7
|
+
import type CommandInspectorComponent from "./CommandInspector.svelte";
|
|
8
|
+
import type EasePickerComponent from "./EasePicker.svelte";
|
|
9
|
+
import type ShotInspectorComponent from "./ShotInspector.svelte";
|
|
10
|
+
import type SceneSettingsComponent from "./SceneSettings.svelte";
|
|
5
11
|
export { default as AnimationEditor } from "./AnimationEditor.svelte";
|
|
6
12
|
export { default as AnimationStage } from "./AnimationStage.svelte";
|
|
7
13
|
export { default as ShotTimeline } from "./ShotTimeline.svelte";
|
|
14
|
+
export { default as PlayheadScrubber } from "./PlayheadScrubber.svelte";
|
|
15
|
+
export { default as CommandList } from "./CommandList.svelte";
|
|
16
|
+
export { default as CommandInspector } from "./CommandInspector.svelte";
|
|
17
|
+
export { default as EasePicker } from "./EasePicker.svelte";
|
|
18
|
+
export { default as ShotInspector } from "./ShotInspector.svelte";
|
|
19
|
+
export { default as SceneSettings } from "./SceneSettings.svelte";
|
|
8
20
|
export * from "./createAnimationEditor.svelte.js";
|
|
9
21
|
export type AnimationEditorProps = ComponentProps<typeof AnimationEditorComponent>;
|
|
10
22
|
export type AnimationStageProps = ComponentProps<typeof AnimationStageComponent>;
|
|
11
23
|
export type ShotTimelineProps = ComponentProps<typeof ShotTimelineComponent>;
|
|
24
|
+
export type PlayheadScrubberProps = ComponentProps<typeof PlayheadScrubberComponent>;
|
|
25
|
+
export type CommandListProps = ComponentProps<typeof CommandListComponent>;
|
|
26
|
+
export type CommandInspectorProps = ComponentProps<typeof CommandInspectorComponent>;
|
|
27
|
+
export type EasePickerProps = ComponentProps<typeof EasePickerComponent>;
|
|
28
|
+
export type ShotInspectorProps = ComponentProps<typeof ShotInspectorComponent>;
|
|
29
|
+
export type SceneSettingsProps = ComponentProps<typeof SceneSettingsComponent>;
|
|
12
30
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/animation-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,wBAAwB,MAAM,0BAA0B,CAAC;AACrE,OAAO,KAAK,uBAAuB,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,qBAAqB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/animation-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,wBAAwB,MAAM,0BAA0B,CAAC;AACrE,OAAO,KAAK,uBAAuB,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,qBAAqB,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,yBAAyB,MAAM,2BAA2B,CAAC;AACvE,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,yBAAyB,MAAM,2BAA2B,CAAC;AACvE,OAAO,KAAK,mBAAmB,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AAEjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,cAAc,mCAAmC,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,wBAAwB,CAAC,CAAC;AACnF,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACjF,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC7E,MAAM,MAAM,qBAAqB,GAAG,cAAc,CAAC,OAAO,yBAAyB,CAAC,CAAC;AACrF,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,qBAAqB,GAAG,cAAc,CAAC,OAAO,yBAAyB,CAAC,CAAC;AACrF,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACzE,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export { default as AnimationEditor } from "./AnimationEditor.svelte";
|
|
2
2
|
export { default as AnimationStage } from "./AnimationStage.svelte";
|
|
3
3
|
export { default as ShotTimeline } from "./ShotTimeline.svelte";
|
|
4
|
+
export { default as PlayheadScrubber } from "./PlayheadScrubber.svelte";
|
|
5
|
+
export { default as CommandList } from "./CommandList.svelte";
|
|
6
|
+
export { default as CommandInspector } from "./CommandInspector.svelte";
|
|
7
|
+
export { default as EasePicker } from "./EasePicker.svelte";
|
|
8
|
+
export { default as ShotInspector } from "./ShotInspector.svelte";
|
|
9
|
+
export { default as SceneSettings } from "./SceneSettings.svelte";
|
|
4
10
|
export * from "./createAnimationEditor.svelte.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"@tiptap/extension-bubble-menu": "^2.11.5",
|
|
124
124
|
"@tiptap/extension-placeholder": "^2.11.5",
|
|
125
125
|
"@tiptap/starter-kit": "^2.11.5",
|
|
126
|
-
"@pixerate/editor": "0.
|
|
126
|
+
"@pixerate/editor": "0.66.0"
|
|
127
127
|
},
|
|
128
128
|
"peerDependencies": {
|
|
129
129
|
"@dagrejs/dagre": "^1.1.4",
|