@pixerate/editor-svelte 0.196.3 → 0.197.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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { AnimationEditorController, MotionEffectResult } from '@pixerate/editor/animation-editor';
3
+ import { pathControlsOf } from '@pixerate/editor/effects';
3
4
  import EffectParameters from '../effects/EffectParameters.svelte';
4
5
  import { effectDescriptionOf } from '../effects/effects.svelte.js';
5
6
 
@@ -8,11 +9,20 @@
8
9
  instance: string;
9
10
  effect: string;
10
11
  values: Readonly<Record<string, unknown>>;
12
+ pathNodes?: readonly string[];
13
+ defaultPathNode?: string;
14
+ disabled?: boolean;
11
15
  }
12
16
 
13
- let { controller, instance, effect, values }: Props = $props();
17
+ let { controller, instance, effect, values, pathNodes = [], defaultPathNode = '', disabled = false }: Props = $props();
14
18
  const loaded = effectDescriptionOf(() => controller.effectsProvider, () => effect);
15
19
  let message = $state<string | null>(null);
20
+ let pathNode = $state('');
21
+ let words = $state('');
22
+ const chosenPathNode = $derived(
23
+ pathNode && pathNodes.includes(pathNode) ? pathNode : defaultPathNode && pathNodes.includes(defaultPathNode) ? defaultPathNode : pathNodes[0] ?? ''
24
+ );
25
+ const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
16
26
  const report = (r: Promise<MotionEffectResult>) => void r.then((x) => (message = x.ok ? null : x.error));
17
27
  </script>
18
28
 
@@ -24,9 +34,45 @@
24
34
  {:else if !loaded.description}
25
35
  <p class="text-[11px] text-muted-foreground">Loading…</p>
26
36
  {:else}
37
+ {@const description = loaded.description}
38
+ {@const pathControls = description.kind === 'vector' ? pathControlsOf(description) : []}
39
+ {#if pathControls.length > 0}
40
+ <div class="mb-1 flex flex-wrap items-center gap-1" data-effect-part="path">
41
+ <span class="text-muted-foreground">Path</span>
42
+ <select
43
+ class="rounded border border-border bg-transparent px-1 py-0.5 text-[11px]"
44
+ aria-label="Path node"
45
+ value={chosenPathNode}
46
+ disabled={disabled || !pathNodes.length}
47
+ onchange={(e) => (pathNode = e.currentTarget.value)}
48
+ >
49
+ <option value="">Path node…</option>
50
+ {#each pathNodes as id (id)}<option value={id}>{id}</option>{/each}
51
+ </select>
52
+ <button type="button" class={btn} disabled={disabled || !chosenPathNode} onclick={() => report(controller.setVectorEffectPath(instance, { node: chosenPathNode }))}>
53
+ Use path node
54
+ </button>
55
+ {#if controller.effectsProvider.pathFromText}
56
+ <input
57
+ class="w-24 rounded border border-border bg-transparent px-1 py-0.5"
58
+ aria-label="Words to write along the path"
59
+ placeholder="Words"
60
+ bind:value={words}
61
+ {disabled}
62
+ />
63
+ <button type="button" class={btn} disabled={disabled || !words.trim()} onclick={() => report(controller.setVectorEffectPath(instance, { text: words }))}>
64
+ Write
65
+ </button>
66
+ {/if}
67
+ <button type="button" class={btn} {disabled} onclick={() => report(controller.updateMotionEffect(instance, { [pathControls[0]]: '' }))}>
68
+ Clear path
69
+ </button>
70
+ </div>
71
+ {/if}
27
72
  <EffectParameters
28
- description={loaded.description}
73
+ {description}
29
74
  {values}
75
+ {disabled}
30
76
  onchange={(k, v, g) => report(controller.updateMotionEffect(instance, { [k]: v as never }, { merge: g ?? undefined }))}
31
77
  onreset={(k) => report(controller.updateMotionEffect(instance, { [k]: null }))}
32
78
  onpreset={(v) => report(controller.updateMotionEffect(instance, v as never))}
@@ -4,6 +4,9 @@ interface Props {
4
4
  instance: string;
5
5
  effect: string;
6
6
  values: Readonly<Record<string, unknown>>;
7
+ pathNodes?: readonly string[];
8
+ defaultPathNode?: string;
9
+ disabled?: boolean;
7
10
  }
8
11
  declare const AnimationEffectControls: import("svelte").Component<Props, {}, "">;
9
12
  type AnimationEffectControls = ReturnType<typeof AnimationEffectControls>;
@@ -16,16 +16,24 @@
16
16
  const view = animationEditorStateOf(() => controller);
17
17
  const ed = $derived(view.current);
18
18
  const provider = $derived(ed.effectsProvider);
19
+ const applied = $derived.by(() => {
20
+ void ed.scene;
21
+ return controller.appliedEffects();
22
+ });
19
23
  const nodes = $derived([...indexMotionNodes(ed.scene.artwork.nodes).keys()]);
24
+ const pathNodes = $derived.by(() => {
25
+ const vectorGroups = new Set(applied.filter((e) => e.kind === 'vector').map((e) => e.instance));
26
+ const own = ed.scene.artwork.nodes.filter((n) => !vectorGroups.has(n.id));
27
+ return [...indexMotionNodes(own).values()].filter((e) => e.node.type === 'path').map((e) => e.node.id);
28
+ });
20
29
  let chosen = $state('');
21
30
  const target = $derived(node && nodes.includes(node) ? node : chosen && nodes.includes(chosen) ? chosen : '');
22
31
  let browse = $state<null | 'motion' | 'vector'>(null);
32
+ let vectorPathNode = $state('');
33
+ let vectorWords = $state('');
34
+ const activeVectorPathNode = $derived(vectorPathNode && pathNodes.includes(vectorPathNode) ? vectorPathNode : '');
23
35
  let open = $state<string | null>(null);
24
36
  let message = $state<string | null>(null);
25
- const applied = $derived.by(() => {
26
- void ed.scene;
27
- return controller.appliedEffects();
28
- });
29
37
  const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
30
38
  function report(r: MotionEffectResult | Promise<MotionEffectResult>) {
31
39
  void Promise.resolve(r).then((x) => (message = x.ok ? (x.notes.length ? x.notes.join(' ') : null) : x.error));
@@ -53,7 +61,46 @@
53
61
  {/if}
54
62
  <EffectTemplates {provider} disabled={ed.readOnly} fillLabel="New scene from template" onfill={(t, v) => controller.fillTemplate(t, v).then((r) => (r.ok && r.notes.length && (message = r.notes.join(' ')), r))} />
55
63
  {#if browse === 'vector'}
56
- <EffectsBrowser {provider} kinds={['vector']} title="Vector effects" onpick={(e) => ((browse = null), report(controller.addVectorEffect(e.id)))} />
64
+ <div class="space-y-1">
65
+ <div class="flex flex-wrap items-center gap-1" data-effect-part="vector-path">
66
+ <span class="text-muted-foreground">Path</span>
67
+ <select
68
+ class="rounded border border-border bg-transparent px-1 py-0.5 text-[11px]"
69
+ aria-label="Path node for vector effect"
70
+ value={activeVectorPathNode}
71
+ disabled={ed.readOnly || !pathNodes.length}
72
+ onchange={(e) => (vectorPathNode = e.currentTarget.value)}
73
+ >
74
+ <option value="">Default path</option>
75
+ {#each pathNodes as id (id)}<option value={id}>{id}</option>{/each}
76
+ </select>
77
+ {#if provider?.pathFromText}
78
+ <input
79
+ class="w-24 rounded border border-border bg-transparent px-1 py-0.5"
80
+ aria-label="Words for vector effect path"
81
+ placeholder="Words"
82
+ bind:value={vectorWords}
83
+ disabled={ed.readOnly}
84
+ />
85
+ {/if}
86
+ </div>
87
+ <EffectsBrowser
88
+ {provider}
89
+ kinds={['vector']}
90
+ title="Vector effects"
91
+ onpick={(e) => {
92
+ browse = null;
93
+ const path = e.accepts?.includes('path')
94
+ ? vectorWords.trim()
95
+ ? { text: vectorWords }
96
+ : activeVectorPathNode
97
+ ? { node: activeVectorPathNode }
98
+ : undefined
99
+ : undefined;
100
+ report(controller.addVectorEffect(e.id, path ? { path } : undefined));
101
+ }}
102
+ />
103
+ </div>
57
104
  {/if}
58
105
  <ol class="space-y-1">
59
106
  {#each applied as e (e.instance)}
@@ -64,7 +111,7 @@
64
111
  </button>
65
112
  <button type="button" class={btn} disabled={ed.readOnly} aria-label="Remove {e.name ?? e.effect}" onclick={() => report(controller.removeMotionEffect(e.instance))}>✕</button>
66
113
  </div>
67
- {#if open === e.instance}<div class="mt-1"><AnimationEffectControls {controller} instance={e.instance} effect={e.effect} values={e.values} /></div>{/if}
114
+ {#if open === e.instance}<div class="mt-1"><AnimationEffectControls {controller} instance={e.instance} effect={e.effect} values={e.values} {pathNodes} defaultPathNode={target} disabled={ed.readOnly} /></div>{/if}
68
115
  </li>
69
116
  {/each}
70
117
  </ol>
@@ -41,8 +41,18 @@ export declare function createCanvasDraw<TNode extends CanvasNode = CanvasNode>(
41
41
  selectBrush: (id: string | null, choice?: {
42
42
  keepColour?: boolean;
43
43
  }) => Promise<import("@pixerate/editor/canvas").CanvasBrushPreset | null>;
44
- /** Sets the colour scheme that picks the side of a brush's light / dark default colour. */
45
- setColorScheme: (scheme: "light" | "dark") => void;
44
+ readonly colorScheme: "light" | "dark";
45
+ /**
46
+ * Sets the colour scheme that picks the side of a brush's light / dark default colour; a
47
+ * stroke colour taken from one (`selectBrush`) follows.
48
+ */
49
+ setColorScheme(scheme: "light" | "dark"): void;
50
+ /** "Keep my colour": choosing a brush keeps the stroke colour. */
51
+ readonly colorLocked: boolean;
52
+ /** Locks the stroke colour ("Keep my colour"): `selectBrush` then keeps it. */
53
+ setColorLocked: (locked: boolean) => void;
54
+ /** What the brush picker is set to (brush, colour, scheme, lock), read now; `canvas.selectBrush` returns it. */
55
+ brushSettings: () => import("@pixerate/editor/canvas").CanvasBrushChoice;
46
56
  };
47
57
  export type CanvasDraw<TNode extends CanvasNode = CanvasNode> = ReturnType<typeof createCanvasDraw<TNode>>;
48
58
  export {};
@@ -5,8 +5,14 @@ import { createCanvasDrawController } from '@pixerate/editor/canvas';
5
5
  export function createCanvasDraw(options) {
6
6
  let tool = $state.raw(options.tool ?? 'select');
7
7
  let activeStroke = $state.raw(null);
8
+ let colorLocked = $state(!!options.colorLocked);
9
+ let colorScheme = $state(options.colorScheme === 'dark' ? 'dark' : 'light');
8
10
  const controller = createCanvasDrawController({
9
11
  ...options,
12
+ onColorLockChange: (locked) => {
13
+ colorLocked = locked;
14
+ options.onColorLockChange?.(locked);
15
+ },
10
16
  onToolChange: (nextTool) => {
11
17
  tool = nextTool;
12
18
  style = controller.style;
@@ -53,7 +59,24 @@ export function createCanvasDraw(options) {
53
59
  * Resolves to the preset; rejects for a brush the canvas can't paint.
54
60
  */
55
61
  selectBrush: controller.selectBrush,
56
- /** Sets the colour scheme that picks the side of a brush's light / dark default colour. */
57
- setColorScheme: controller.setColorScheme
62
+ get colorScheme() {
63
+ return colorScheme;
64
+ },
65
+ /**
66
+ * Sets the colour scheme that picks the side of a brush's light / dark default colour; a
67
+ * stroke colour taken from one (`selectBrush`) follows.
68
+ */
69
+ setColorScheme(scheme) {
70
+ controller.setColorScheme(scheme);
71
+ colorScheme = controller.colorScheme;
72
+ },
73
+ /** "Keep my colour": choosing a brush keeps the stroke colour. */
74
+ get colorLocked() {
75
+ return colorLocked;
76
+ },
77
+ /** Locks the stroke colour ("Keep my colour"): `selectBrush` then keeps it. */
78
+ setColorLocked: controller.setColorLocked,
79
+ /** What the brush picker is set to (brush, colour, scheme, lock), read now; `canvas.selectBrush` returns it. */
80
+ brushSettings: controller.brushSettings
58
81
  };
59
82
  }
@@ -211,6 +211,12 @@
211
211
  </div>
212
212
  <!-- Top layer first, as it draws. -->
213
213
  {#each [...layers].reverse() as l (`${l.kind}-${l.index}`)}{@render layerRow(l, layers.length, ids)}{/each}
214
+ {#if kind === 'stroke' && layers.some((l) => l.brushstroke)}
215
+ <!-- Editor state: brushstrokes added or rebrushed from now on keep their colour. -->
216
+ <label class="flex items-center gap-1 text-[11px] text-muted-foreground" title="Choosing a brush keeps the stroke's colour instead of taking the brush's default colour">
217
+ <input type="checkbox" aria-label="Keep my colour" data-vector-keep-colour="" checked={view.colorLocked} onchange={(e) => controller.setColorLocked(e.currentTarget.checked)} /> Keep my colour when choosing a brush
218
+ </label>
219
+ {/if}
214
220
  </div>
215
221
  {/snippet}
216
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.196.3",
3
+ "version": "0.197.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -175,7 +175,7 @@
175
175
  "**/*.css"
176
176
  ],
177
177
  "dependencies": {
178
- "@pixerate/editor": "0.266.2"
178
+ "@pixerate/editor": "0.267.0"
179
179
  },
180
180
  "peerDependencies": {
181
181
  "@dagrejs/dagre": "^1.1.4",