@pixerate/editor-svelte 0.196.3 → 0.198.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/AnimationEffectControls.svelte +48 -2
- package/dist/animation-editor/AnimationEffectControls.svelte.d.ts +3 -0
- package/dist/animation-editor/AnimationEffectsPanel.svelte +53 -6
- package/dist/brushes/BrushEditor.svelte +38 -3
- package/dist/brushes/BrushEditor.svelte.d.ts +8 -1
- package/dist/brushes/BrushLibraryEditor.svelte +18 -2
- package/dist/canvas/createCanvasDraw.svelte.d.ts +12 -2
- package/dist/canvas/createCanvasDraw.svelte.js +25 -2
- package/dist/paint-editor/BrushPanel.svelte +17 -3
- package/dist/vector/VectorAppearancePanel.svelte +6 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import { brushEditorScope, brushEditorSections, PAINT_PROFILE, setBrushEditorValue, type Brush, type BrushHostProfile } from '@pixerate/editor/brushes';
|
|
3
|
+
import { brushEditorScope, brushEditorSections, PAINT_PROFILE, setBrushEditorValue, withBrushSource, type Brush, type BrushHostProfile, type BrushSource } from '@pixerate/editor/brushes';
|
|
4
4
|
import PaintSlider from '../paint-editor/PaintSlider.svelte';
|
|
5
5
|
import PixelCanvas from '../paint-editor/PixelCanvas.svelte';
|
|
6
6
|
import { button, chip } from './styles.js';
|
|
@@ -27,10 +27,17 @@
|
|
|
27
27
|
onsaveas?: (name: string) => unknown;
|
|
28
28
|
/** Extra controls a host adds to a section (e.g. the sticker picker for stamps). */
|
|
29
29
|
extra?: Snippet<[string]>;
|
|
30
|
+
/** The library's image sources, offered as stamps and as grain. */
|
|
31
|
+
sources?: ReadonlyArray<Pick<BrushSource, 'id' | 'name'>>;
|
|
32
|
+
/**
|
|
33
|
+
* With it, "Add image…" buttons for an image tip and for grain: it stores the picked file as a
|
|
34
|
+
* source (e.g. `addBrushSourceFromFile`) and returns its `source:<id>` ref.
|
|
35
|
+
*/
|
|
36
|
+
onaddsource?: (file: File) => Promise<string>;
|
|
30
37
|
class?: string;
|
|
31
38
|
}
|
|
32
|
-
let { brush, profile = PAINT_PROFILE, onchange, preview, previewBackground, onsave, saveLabel = 'Save', onsaveas, extra, class: className = '' }: Props = $props();
|
|
33
|
-
const sections = $derived(brushEditorSections(brush, profile));
|
|
39
|
+
let { brush, profile = PAINT_PROFILE, onchange, preview, previewBackground, onsave, saveLabel = 'Save', onsaveas, extra, sources, onaddsource, class: className = '' }: Props = $props();
|
|
40
|
+
const sections = $derived(brushEditorSections(brush, profile, { sources }));
|
|
34
41
|
const scope = $derived(brushEditorScope(brush, profile));
|
|
35
42
|
let problems = $state<string | null>(null);
|
|
36
43
|
let name = $state('');
|
|
@@ -39,6 +46,17 @@
|
|
|
39
46
|
problems = r.problems.length ? r.problems.join(' ') : null;
|
|
40
47
|
if (r.brush !== brush) onchange(r.brush);
|
|
41
48
|
};
|
|
49
|
+
/** Stores a picked image as a source and makes it the tip or the grain. */
|
|
50
|
+
const addSource = async (file: File | undefined, as: 'tip' | 'grain') => {
|
|
51
|
+
if (!file || !onaddsource) return;
|
|
52
|
+
try {
|
|
53
|
+
const r = withBrushSource(brush, await onaddsource(file), as);
|
|
54
|
+
problems = r.problems.length ? r.problems.join(' ') : null;
|
|
55
|
+
onchange(r.brush);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
problems = (e as Error).message;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
42
60
|
const setColour = (path: string, light: string, dark: string | null) => set(path, dark === null ? light : { light, dark });
|
|
43
61
|
const setAt = (path: string, list: Array<string | { light: string; dark: string }>, i: number, entry: string | { light: string; dark: string }) => set(path, list.map((x, j) => (j === i ? entry : x)));
|
|
44
62
|
const attemptSave = async (fn: () => unknown, after?: () => void) => {
|
|
@@ -140,6 +158,23 @@
|
|
|
140
158
|
{/each}
|
|
141
159
|
</div>
|
|
142
160
|
{/if}
|
|
161
|
+
{#if onaddsource && (c.path === 'tip.stamps' || c.path === 'tip.grain.source')}
|
|
162
|
+
{@const label = c.path === 'tip.stamps' ? 'Add image tip…' : 'Add grain image…'}
|
|
163
|
+
<label class={`${button} cursor-pointer self-start`}>
|
|
164
|
+
{label}
|
|
165
|
+
<input
|
|
166
|
+
type="file"
|
|
167
|
+
accept="image/*"
|
|
168
|
+
aria-label={label}
|
|
169
|
+
class="sr-only"
|
|
170
|
+
onchange={(e) => {
|
|
171
|
+
const file = e.currentTarget.files?.[0];
|
|
172
|
+
e.currentTarget.value = '';
|
|
173
|
+
void addSource(file, c.path === 'tip.stamps' ? 'tip' : 'grain');
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</label>
|
|
177
|
+
{/if}
|
|
143
178
|
{/each}
|
|
144
179
|
{#if extra}{@render extra(s.id)}{/if}
|
|
145
180
|
</section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import { type Brush, type BrushHostProfile } from '@pixerate/editor/brushes';
|
|
2
|
+
import { type Brush, type BrushHostProfile, type BrushSource } from '@pixerate/editor/brushes';
|
|
3
3
|
interface Props {
|
|
4
4
|
/** The brush being edited, as facets. */
|
|
5
5
|
brush: Brush;
|
|
@@ -26,6 +26,13 @@ interface Props {
|
|
|
26
26
|
onsaveas?: (name: string) => unknown;
|
|
27
27
|
/** Extra controls a host adds to a section (e.g. the sticker picker for stamps). */
|
|
28
28
|
extra?: Snippet<[string]>;
|
|
29
|
+
/** The library's image sources, offered as stamps and as grain. */
|
|
30
|
+
sources?: ReadonlyArray<Pick<BrushSource, 'id' | 'name'>>;
|
|
31
|
+
/**
|
|
32
|
+
* With it, "Add image…" buttons for an image tip and for grain: it stores the picked file as a
|
|
33
|
+
* source (e.g. `addBrushSourceFromFile`) and returns its `source:<id>` ref.
|
|
34
|
+
*/
|
|
35
|
+
onaddsource?: (file: File) => Promise<string>;
|
|
29
36
|
class?: string;
|
|
30
37
|
}
|
|
31
38
|
declare const BrushEditor: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { brushPreviewColours, checkBrushEdit, newBrushFor, saveBrushEdit, type Brush, type BrushHostProfile, type BrushLibrary, type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
2
|
+
import { addBrushSourceFromFile, brushPreviewColours, checkBrushEdit, newBrushFor, saveBrushEdit, type Brush, type BrushHostProfile, type BrushLibrary, type BrushPreviewRenderer, type BrushSource } from '@pixerate/editor/brushes';
|
|
3
3
|
import { button } from './styles.js';
|
|
4
4
|
import BrushEditor from './BrushEditor.svelte';
|
|
5
5
|
|
|
@@ -32,6 +32,20 @@
|
|
|
32
32
|
let draft = $state<Brush | null>(null);
|
|
33
33
|
let status = $state<string | null>(null);
|
|
34
34
|
let preview = $state<string | null>(null);
|
|
35
|
+
let sources = $state<BrushSource[]>([]);
|
|
36
|
+
|
|
37
|
+
// The library's image sources, for image tips and grain (kept current as sources are added).
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const lib = library;
|
|
40
|
+
let live = true;
|
|
41
|
+
const read = () => void lib.listSources().then((list) => live && (sources = list), () => undefined);
|
|
42
|
+
read();
|
|
43
|
+
const off = lib.subscribe(read);
|
|
44
|
+
return () => {
|
|
45
|
+
live = false;
|
|
46
|
+
off();
|
|
47
|
+
};
|
|
48
|
+
});
|
|
35
49
|
|
|
36
50
|
$effect(() => {
|
|
37
51
|
const lib = library;
|
|
@@ -91,7 +105,7 @@
|
|
|
91
105
|
BrushEditor on a brush library for one host: loads the brush, edits a draft and saves it with saveBrushEdit. A saved
|
|
92
106
|
brush is changed in place, a built-in is saved as a copy, "Save brush" saves under a new name, and "New brush" starts
|
|
93
107
|
from the current one with only what the host uses. Facets the host can't use are kept on save; a new brush must pass
|
|
94
|
-
the host's check.
|
|
108
|
+
the host's check. Images added for an image tip or grain are stored in the library as sources.
|
|
95
109
|
-->
|
|
96
110
|
{#if draft}
|
|
97
111
|
<div class={`flex flex-col gap-2 text-xs text-slate-200 ${className}`} data-testid="brush-library-editor">
|
|
@@ -114,6 +128,8 @@
|
|
|
114
128
|
onsave={original ? () => save() : undefined}
|
|
115
129
|
saveLabel={builtin ? 'Save a copy' : 'Save'}
|
|
116
130
|
onsaveas={(name) => save(name)}
|
|
131
|
+
{sources}
|
|
132
|
+
onaddsource={async (file) => (await addBrushSourceFromFile(library, file)).ref}
|
|
117
133
|
/>
|
|
118
134
|
{#if errors.length}
|
|
119
135
|
<ul class="text-[11px] text-rose-400" data-testid="brush-library-editor-issues">
|
|
@@ -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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import StickerPicker from './StickerPicker.svelte';
|
|
3
3
|
import { brushLoadColor, brushTakesPaint, downloadBytes, toggleStickerRef, DEFAULT_PAINT_GRADIENT, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
4
|
-
import { brushColourHex, brushPreviewColours, PAINT_PROFILE, type Brush, type BrushLibraryEntry } from '@pixerate/editor/brushes';
|
|
4
|
+
import { addBrushSourceFromFile, brushColourHex, brushPreviewColours, PAINT_PROFILE, type Brush, type BrushLibraryEntry } from '@pixerate/editor/brushes';
|
|
5
5
|
import { untrack } from 'svelte';
|
|
6
6
|
import BrushEditor from '../brushes/BrushEditor.svelte';
|
|
7
7
|
import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
void brush;
|
|
40
40
|
return untrack(() => controller.getBrushSpec()!);
|
|
41
41
|
});
|
|
42
|
+
// The image sources the editor paints with, read again after every change (sources added to the library).
|
|
43
|
+
const sources = $derived.by(() => {
|
|
44
|
+
void snap;
|
|
45
|
+
return untrack(() => controller.brushSources.all());
|
|
46
|
+
});
|
|
42
47
|
// Previewed in the brush's own default colour when it has one, else the paint colour.
|
|
43
48
|
const own = $derived(typeof spec.colour?.default === 'string' && /^#[0-9a-f]{6}/i.test(spec.colour.default) ? spec.colour.default.slice(0, 7) : undefined);
|
|
44
49
|
const preview = $derived.by(() => {
|
|
@@ -92,10 +97,19 @@
|
|
|
92
97
|
{/if}
|
|
93
98
|
{/snippet}
|
|
94
99
|
|
|
95
|
-
<!-- The brush library (BrushLibraryBrowser) and the active brush in the BrushEditor, plus the brush gradient, sticker stamps, what the brush carries, import and export. -->
|
|
100
|
+
<!-- The brush library (BrushLibraryBrowser) and the active brush in the BrushEditor, plus the brush gradient, sticker stamps, what the brush carries, import and export. Images picked for an image tip or grain go into the brush library as sources. -->
|
|
96
101
|
<div class={`${panel} ${className}`} data-testid="brush-panel">
|
|
97
102
|
<BrushLibraryBrowser {entries} activeId={brush.id} onselect={(id) => controller.setBrush({ id })} thumbnail={(id) => thumbs.get(id) ?? null} {thumbnailWidth} {thumbnailHeight} />
|
|
98
|
-
<BrushEditor
|
|
103
|
+
<BrushEditor
|
|
104
|
+
brush={spec}
|
|
105
|
+
profile={PAINT_PROFILE}
|
|
106
|
+
{preview}
|
|
107
|
+
onchange={(next) => run(() => controller.setBrushSpec(next))}
|
|
108
|
+
onsaveas={(name) => controller.saveBrush({ id: slug(name), name })}
|
|
109
|
+
sources={sources}
|
|
110
|
+
onaddsource={async (file) => (await addBrushSourceFromFile(controller.brushLibrary, file)).ref}
|
|
111
|
+
{extra}
|
|
112
|
+
/>
|
|
99
113
|
<div class="flex flex-wrap items-center gap-1">
|
|
100
114
|
<button type="button" class={button} onclick={() => run(async () => downloadBytes(JSON.stringify(await controller.exportBrushes(), null, 2), 'brushes.json', 'application/json'))}>Export</button>
|
|
101
115
|
<button type="button" class={button} onclick={() => fileInput?.click()}>Import…</button>
|
|
@@ -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.
|
|
3
|
+
"version": "0.198.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.
|
|
178
|
+
"@pixerate/editor": "0.268.0"
|
|
179
179
|
},
|
|
180
180
|
"peerDependencies": {
|
|
181
181
|
"@dagrejs/dagre": "^1.1.4",
|