@pixerate/editor-svelte 0.194.0 → 0.195.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/{paint-editor → brushes}/BrushEditor.svelte +37 -16
- package/dist/{paint-editor → brushes}/BrushEditor.svelte.d.ts +10 -3
- package/dist/brushes/BrushLibraryEditor.svelte +117 -0
- package/dist/brushes/BrushLibraryEditor.svelte.d.ts +21 -0
- package/dist/brushes/index.d.ts +6 -0
- package/dist/brushes/index.js +2 -0
- package/dist/brushes/styles.d.ts +3 -0
- package/dist/brushes/styles.js +3 -0
- package/dist/image-editor/BrushOptionsBar.svelte +24 -3
- package/dist/image-editor/BrushOptionsBar.svelte.d.ts +3 -0
- package/dist/paint-editor/BrushPanel.svelte +1 -1
- package/dist/paint-editor/PaintSlider.svelte +3 -1
- package/dist/paint-editor/PaintSlider.svelte.d.ts +7 -1
- package/dist/paint-editor/index.d.ts +2 -2
- package/dist/paint-editor/index.js +2 -1
- package/dist/paint-editor/paintEditorRune.svelte.d.ts +1 -2
- package/dist/paint-editor/paintEditorRune.svelte.js +1 -2
- package/dist/vector/VectorAppearancePanel.svelte +38 -2
- package/dist/vector/VectorAppearancePanel.svelte.d.ts +7 -1
- package/package.json +2 -2
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import { brushEditorSections, PAINT_PROFILE, setBrushEditorValue, type Brush, type BrushHostProfile } from '@pixerate/editor/brushes';
|
|
4
|
-
import PaintSlider from '
|
|
5
|
-
import PixelCanvas from '
|
|
6
|
-
import { button, chip } from './
|
|
3
|
+
import { brushEditorScope, brushEditorSections, PAINT_PROFILE, setBrushEditorValue, type Brush, type BrushHostProfile } from '@pixerate/editor/brushes';
|
|
4
|
+
import PaintSlider from '../paint-editor/PaintSlider.svelte';
|
|
5
|
+
import PixelCanvas from '../paint-editor/PixelCanvas.svelte';
|
|
6
|
+
import { button, chip } from './styles.js';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
/** The brush being edited, as facets. */
|
|
10
10
|
brush: Brush;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* What the host renders (default the paint editor): only its sections, controls and options
|
|
13
|
+
* are shown. Facets of the brush it can't use stay as they are, named in a note.
|
|
14
|
+
*/
|
|
12
15
|
profile?: BrushHostProfile;
|
|
13
16
|
/** Called with the whole brush after each change (normalised). */
|
|
14
17
|
onchange: (brush: Brush) => void;
|
|
15
|
-
/** A live preview stroke of `brush
|
|
16
|
-
preview?: { width: number; height: number; data: Uint8ClampedArray } | null;
|
|
18
|
+
/** A live preview stroke of `brush`: pixels (e.g. `controller.brushThumbnail`) or an image URL. */
|
|
19
|
+
preview?: { width: number; height: number; data: Uint8ClampedArray } | string | null;
|
|
20
|
+
/** With it, a "Save" button (labelled `saveLabel`) saves the brush in place. */
|
|
21
|
+
onsave?: () => unknown;
|
|
22
|
+
/** The Save button's label (default "Save"; e.g. "Save a copy" for a built-in). */
|
|
23
|
+
saveLabel?: string;
|
|
17
24
|
/** With it, a name field and "Save brush" save the brush under that name. */
|
|
18
25
|
onsaveas?: (name: string) => unknown;
|
|
19
26
|
/** Extra controls a host adds to a section (e.g. the sticker picker for stamps). */
|
|
20
27
|
extra?: Snippet<[string]>;
|
|
21
28
|
class?: string;
|
|
22
29
|
}
|
|
23
|
-
let { brush, profile = PAINT_PROFILE, onchange, preview, onsaveas, extra, class: className = '' }: Props = $props();
|
|
30
|
+
let { brush, profile = PAINT_PROFILE, onchange, preview, onsave, saveLabel = 'Save', onsaveas, extra, class: className = '' }: Props = $props();
|
|
24
31
|
const sections = $derived(brushEditorSections(brush, profile));
|
|
32
|
+
const scope = $derived(brushEditorScope(brush, profile));
|
|
25
33
|
let problems = $state<string | null>(null);
|
|
26
34
|
let name = $state('');
|
|
27
35
|
const set = (path: string, value: unknown) => {
|
|
@@ -31,22 +39,32 @@
|
|
|
31
39
|
};
|
|
32
40
|
const setColour = (path: string, light: string, dark: string | null) => set(path, dark === null ? light : { light, dark });
|
|
33
41
|
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)));
|
|
34
|
-
const
|
|
42
|
+
const attemptSave = async (fn: () => unknown, after?: () => void) => {
|
|
35
43
|
try {
|
|
36
|
-
await
|
|
37
|
-
|
|
44
|
+
await fn();
|
|
45
|
+
after?.();
|
|
38
46
|
problems = null;
|
|
39
47
|
} catch (e) {
|
|
40
48
|
problems = (e as Error).message;
|
|
41
49
|
}
|
|
42
50
|
};
|
|
51
|
+
const save = () => attemptSave(() => onsaveas?.(name.trim()), () => (name = ''));
|
|
43
52
|
</script>
|
|
44
53
|
|
|
45
|
-
<!--
|
|
54
|
+
<!--
|
|
55
|
+
Edits a brush by its facets (brushEditorSections), with a live preview stroke, "Save" and "Save as". It needs no
|
|
56
|
+
editor controller, so every host that paints uses it. Only what the host profile uses is shown; what the brush has
|
|
57
|
+
for other hosts is kept and named (brushEditorScope). BrushLibraryEditor wires it to a brush library.
|
|
58
|
+
-->
|
|
46
59
|
<div class={`flex flex-col gap-2 ${className}`} data-testid="brush-editor">
|
|
47
|
-
{#if preview
|
|
60
|
+
{#if typeof preview === 'string'}
|
|
61
|
+
<img src={preview} alt="Brush preview" class="rounded object-contain" style="width: 100%; aspect-ratio: 4 / 1" />
|
|
62
|
+
{:else if preview !== undefined}
|
|
48
63
|
<PixelCanvas pixels={preview} label="Brush preview" class="rounded" style={`width: 100%; aspect-ratio: ${preview ? `${preview.width} / ${preview.height}` : '4 / 1'}`} />
|
|
49
64
|
{/if}
|
|
65
|
+
{#if scope.note}
|
|
66
|
+
<p class="text-[10px] text-amber-400" data-testid="brush-editor-scope" title={`${profile.name} shows only what it can use; these are kept unchanged when you save.`}>{scope.note}</p>
|
|
67
|
+
{/if}
|
|
50
68
|
{#each sections as s (s.id)}
|
|
51
69
|
<section class="flex flex-col gap-1.5" data-section={s.id} data-testid={s.id === 'behaviour' ? 'brush-engine' : undefined}>
|
|
52
70
|
<span class="font-medium text-slate-300">{s.title}</span>
|
|
@@ -124,10 +142,13 @@
|
|
|
124
142
|
{#if extra}{@render extra(s.id)}{/if}
|
|
125
143
|
</section>
|
|
126
144
|
{/each}
|
|
127
|
-
{#if onsaveas}
|
|
145
|
+
{#if onsave || onsaveas}
|
|
128
146
|
<div class="flex flex-wrap items-center gap-1">
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
{#if onsave}<button type="button" class={button} onclick={() => attemptSave(onsave)}>{saveLabel}</button>{/if}
|
|
148
|
+
{#if onsaveas}
|
|
149
|
+
<input bind:value={name} placeholder="Brush name" aria-label="Brush name" class="w-28 rounded bg-slate-800 px-1.5 py-0.5" />
|
|
150
|
+
<button type="button" class={button} disabled={!name.trim()} onclick={save}>Save brush</button>
|
|
151
|
+
{/if}
|
|
131
152
|
</div>
|
|
132
153
|
{/if}
|
|
133
154
|
{#if problems}<p role="alert" class="text-[11px] text-rose-400">{problems}</p>{/if}
|
|
@@ -3,16 +3,23 @@ import { type Brush, type BrushHostProfile } from '@pixerate/editor/brushes';
|
|
|
3
3
|
interface Props {
|
|
4
4
|
/** The brush being edited, as facets. */
|
|
5
5
|
brush: Brush;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* What the host renders (default the paint editor): only its sections, controls and options
|
|
8
|
+
* are shown. Facets of the brush it can't use stay as they are, named in a note.
|
|
9
|
+
*/
|
|
7
10
|
profile?: BrushHostProfile;
|
|
8
11
|
/** Called with the whole brush after each change (normalised). */
|
|
9
12
|
onchange: (brush: Brush) => void;
|
|
10
|
-
/** A live preview stroke of `brush
|
|
13
|
+
/** A live preview stroke of `brush`: pixels (e.g. `controller.brushThumbnail`) or an image URL. */
|
|
11
14
|
preview?: {
|
|
12
15
|
width: number;
|
|
13
16
|
height: number;
|
|
14
17
|
data: Uint8ClampedArray;
|
|
15
|
-
} | null;
|
|
18
|
+
} | string | null;
|
|
19
|
+
/** With it, a "Save" button (labelled `saveLabel`) saves the brush in place. */
|
|
20
|
+
onsave?: () => unknown;
|
|
21
|
+
/** The Save button's label (default "Save"; e.g. "Save a copy" for a built-in). */
|
|
22
|
+
saveLabel?: string;
|
|
16
23
|
/** With it, a name field and "Save brush" save the brush under that name. */
|
|
17
24
|
onsaveas?: (name: string) => unknown;
|
|
18
25
|
/** Extra controls a host adds to a section (e.g. the sticker picker for stamps). */
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { brushColourHex, checkBrushEdit, newBrushFor, saveBrushEdit, type Brush, type BrushHostProfile, type BrushLibrary, type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
3
|
+
import { button } from './styles.js';
|
|
4
|
+
import BrushEditor from './BrushEditor.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** The shared brush library: saves go here, so every editor on it gets the brush. */
|
|
8
|
+
library: BrushLibrary;
|
|
9
|
+
/** The brush to edit (a library id); `null` or absent starts a new brush for the host. */
|
|
10
|
+
brushId?: string | null;
|
|
11
|
+
/** The host the brush is edited for: the editor shows only what it uses. */
|
|
12
|
+
profile: BrushHostProfile;
|
|
13
|
+
/** Draws the preview stroke (the host's renderer, as for `preview_brush`). Without it there is no preview. */
|
|
14
|
+
renderPreview?: BrushPreviewRenderer;
|
|
15
|
+
/** The preview colour for brushes with no solid default colour (default slate). */
|
|
16
|
+
previewColor?: string;
|
|
17
|
+
/** Called with the brush as saved (a copy has a new id): e.g. pick it in the host. */
|
|
18
|
+
onsaved?: (brush: Brush) => void;
|
|
19
|
+
/** With it, a Close button. */
|
|
20
|
+
onclose?: () => void;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
let { library, brushId = null, profile, renderPreview, previewColor = '#64748b', onsaved, onclose, class: className = '' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
let original = $state<Brush | null>(null);
|
|
26
|
+
let builtin = $state(false);
|
|
27
|
+
let draft = $state<Brush | null>(null);
|
|
28
|
+
let status = $state<string | null>(null);
|
|
29
|
+
let preview = $state<string | null>(null);
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
const lib = library;
|
|
33
|
+
const id = brushId;
|
|
34
|
+
const p = profile;
|
|
35
|
+
let live = true;
|
|
36
|
+
status = null;
|
|
37
|
+
void (async () => {
|
|
38
|
+
const entry = id ? await lib.get(id) : null;
|
|
39
|
+
if (!live) return;
|
|
40
|
+
original = entry?.brush ?? null;
|
|
41
|
+
builtin = !!entry?.builtin;
|
|
42
|
+
draft = entry?.brush ?? newBrushFor(p);
|
|
43
|
+
})();
|
|
44
|
+
return () => {
|
|
45
|
+
live = false;
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const colour = $derived((draft && brushColourHex(draft.colour?.default ?? '', 'light')) || previewColor);
|
|
50
|
+
$effect(() => {
|
|
51
|
+
const d = draft;
|
|
52
|
+
const render = renderPreview;
|
|
53
|
+
const c = colour;
|
|
54
|
+
if (!d || !render) return;
|
|
55
|
+
let live = true;
|
|
56
|
+
Promise.resolve(render($state.snapshot(d) as Brush, { width: 240, height: 60, color: c.slice(0, 7) }))
|
|
57
|
+
.then((img) => live && (preview = img.data ? `data:${img.mimeType};base64,${img.data}` : null))
|
|
58
|
+
.catch(() => live && (preview = null));
|
|
59
|
+
return () => {
|
|
60
|
+
live = false;
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const errors = $derived(draft ? checkBrushEdit(original, draft, profile).issues.filter((i) => i.severity === 'error') : []);
|
|
65
|
+
|
|
66
|
+
const save = async (name?: string) => {
|
|
67
|
+
const saved = await saveBrushEdit(library, { original: original && ($state.snapshot(original) as Brush), brush: $state.snapshot(draft!) as Brush, profile, ...(name !== undefined ? { name } : {}) });
|
|
68
|
+
original = saved.brush;
|
|
69
|
+
builtin = false;
|
|
70
|
+
draft = saved.brush;
|
|
71
|
+
status = `Saved “${saved.brush.name}”${saved.kept.length ? `, keeping what ${profile.name} can't use` : ''}.`;
|
|
72
|
+
onsaved?.(saved.brush);
|
|
73
|
+
};
|
|
74
|
+
const startNew = () => {
|
|
75
|
+
const from = draft ? ($state.snapshot(draft) as Brush) : null;
|
|
76
|
+
original = null;
|
|
77
|
+
builtin = false;
|
|
78
|
+
draft = newBrushFor(profile, from);
|
|
79
|
+
status = null;
|
|
80
|
+
};
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<!--
|
|
84
|
+
BrushEditor on a brush library for one host: loads the brush, edits a draft and saves it with saveBrushEdit. A saved
|
|
85
|
+
brush is changed in place, a built-in is saved as a copy, "Save brush" saves under a new name, and "New brush" starts
|
|
86
|
+
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
|
|
87
|
+
the host's check.
|
|
88
|
+
-->
|
|
89
|
+
{#if draft}
|
|
90
|
+
<div class={`flex flex-col gap-2 text-xs text-slate-200 ${className}`} data-testid="brush-library-editor">
|
|
91
|
+
<div class="flex items-center justify-between gap-2">
|
|
92
|
+
<span class="font-medium" data-testid="brush-library-editor-title">{original ? `${draft.name}${builtin ? ' (built in: saves a copy)' : ''}` : 'New brush'}</span>
|
|
93
|
+
<span class="flex gap-1">
|
|
94
|
+
<button type="button" class={button} title={`A new brush from this one, with only what ${profile.name} uses`} onclick={startNew}>New brush</button>
|
|
95
|
+
{#if onclose}<button type="button" class={button} onclick={onclose}>Close</button>{/if}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<BrushEditor
|
|
99
|
+
brush={draft}
|
|
100
|
+
{profile}
|
|
101
|
+
preview={renderPreview ? preview : undefined}
|
|
102
|
+
onchange={(next) => {
|
|
103
|
+
draft = next;
|
|
104
|
+
status = null;
|
|
105
|
+
}}
|
|
106
|
+
onsave={original ? () => save() : undefined}
|
|
107
|
+
saveLabel={builtin ? 'Save a copy' : 'Save'}
|
|
108
|
+
onsaveas={(name) => save(name)}
|
|
109
|
+
/>
|
|
110
|
+
{#if errors.length}
|
|
111
|
+
<ul class="text-[11px] text-rose-400" data-testid="brush-library-editor-issues">
|
|
112
|
+
{#each errors as i (i.facet)}<li>{i.reason} {i.fix}</li>{/each}
|
|
113
|
+
</ul>
|
|
114
|
+
{/if}
|
|
115
|
+
{#if status}<p role="status" class="text-[11px] text-emerald-400">{status}</p>{/if}
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Brush, type BrushHostProfile, type BrushLibrary, type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The shared brush library: saves go here, so every editor on it gets the brush. */
|
|
4
|
+
library: BrushLibrary;
|
|
5
|
+
/** The brush to edit (a library id); `null` or absent starts a new brush for the host. */
|
|
6
|
+
brushId?: string | null;
|
|
7
|
+
/** The host the brush is edited for: the editor shows only what it uses. */
|
|
8
|
+
profile: BrushHostProfile;
|
|
9
|
+
/** Draws the preview stroke (the host's renderer, as for `preview_brush`). Without it there is no preview. */
|
|
10
|
+
renderPreview?: BrushPreviewRenderer;
|
|
11
|
+
/** The preview colour for brushes with no solid default colour (default slate). */
|
|
12
|
+
previewColor?: string;
|
|
13
|
+
/** Called with the brush as saved (a copy has a new id): e.g. pick it in the host. */
|
|
14
|
+
onsaved?: (brush: Brush) => void;
|
|
15
|
+
/** With it, a Close button. */
|
|
16
|
+
onclose?: () => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const BrushLibraryEditor: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type BrushLibraryEditor = ReturnType<typeof BrushLibraryEditor>;
|
|
21
|
+
export default BrushLibraryEditor;
|
package/dist/brushes/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import type BrushEditorComponent from './BrushEditor.svelte';
|
|
2
3
|
import type BrushLibraryBrowserComponent from './BrushLibraryBrowser.svelte';
|
|
4
|
+
import type BrushLibraryEditorComponent from './BrushLibraryEditor.svelte';
|
|
5
|
+
export { default as BrushEditor } from './BrushEditor.svelte';
|
|
3
6
|
export { default as BrushLibraryBrowser } from './BrushLibraryBrowser.svelte';
|
|
7
|
+
export { default as BrushLibraryEditor } from './BrushLibraryEditor.svelte';
|
|
4
8
|
export { brushLibraryState } from './brushLibrary.svelte.js';
|
|
9
|
+
export type BrushEditorProps = ComponentProps<typeof BrushEditorComponent>;
|
|
5
10
|
export type BrushLibraryBrowserProps = ComponentProps<typeof BrushLibraryBrowserComponent>;
|
|
11
|
+
export type BrushLibraryEditorProps = ComponentProps<typeof BrushLibraryEditorComponent>;
|
package/dist/brushes/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
export { default as BrushEditor } from './BrushEditor.svelte';
|
|
1
2
|
export { default as BrushLibraryBrowser } from './BrushLibraryBrowser.svelte';
|
|
3
|
+
export { default as BrushLibraryEditor } from './BrushLibraryEditor.svelte';
|
|
2
4
|
export { brushLibraryState } from './brushLibrary.svelte.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Button classes shared by the brush components and the paint editor's panels. */
|
|
2
|
+
export const chip = (on) => `px-2 py-1 rounded text-[11px] ${on ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-300 hover:text-white'}`;
|
|
3
|
+
export const button = 'px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700 disabled:opacity-40';
|
|
@@ -24,18 +24,21 @@
|
|
|
24
24
|
import { DEFAULT_PAINT_GRADIENT } from '@pixerate/editor/image-editor';
|
|
25
25
|
import PaintGradientEditor from './PaintGradientEditor.svelte';
|
|
26
26
|
import ControlSlider from './ControlSlider.svelte';
|
|
27
|
-
import { IMAGE_PROFILE } from '@pixerate/editor/brushes';
|
|
27
|
+
import { IMAGE_PROFILE, type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
28
28
|
import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
|
|
29
|
+
import BrushLibraryEditor from '../brushes/BrushLibraryEditor.svelte';
|
|
29
30
|
import { brushLibraryState } from '../brushes/brushLibrary.svelte.js';
|
|
30
31
|
|
|
31
32
|
interface Props {
|
|
32
33
|
controller: ImageEditorController;
|
|
33
34
|
/** The row of paint tool buttons (default true); hide it when the host has its own toolbar. */
|
|
34
35
|
showTools?: boolean;
|
|
36
|
+
/** Draws the brush editor's preview stroke (default: the controller's `brushPreview` option). */
|
|
37
|
+
renderBrushPreview?: BrushPreviewRenderer;
|
|
35
38
|
class?: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
let { controller, showTools = true, class: className = '' }: Props = $props();
|
|
41
|
+
let { controller, showTools = true, renderBrushPreview, class: className = '' }: Props = $props();
|
|
39
42
|
|
|
40
43
|
let tick = $state(0);
|
|
41
44
|
$effect(() => {
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
});
|
|
51
54
|
let error = $state<string | null>(null);
|
|
52
55
|
let presetsOpen = $state(false);
|
|
56
|
+
let editing = $state(false);
|
|
53
57
|
// Read only while open: the list follows the library, so a brush saved in another editor shows up.
|
|
54
58
|
const presets = brushLibraryState(() => (presetsOpen ? controller.brushLibrary : null), () => ({ profile: IMAGE_PROFILE }));
|
|
55
59
|
const presetName = $derived(presets.entries.find((e) => e.brush.id === view.brushPreset)?.brush.name ?? view.brushPreset);
|
|
@@ -82,7 +86,24 @@
|
|
|
82
86
|
|
|
83
87
|
{#if view.tool === 'brush' || view.tool === 'mixer' || view.tool === 'eraser'}
|
|
84
88
|
<div class="flex flex-col gap-1" data-testid="brush-presets">
|
|
85
|
-
<
|
|
89
|
+
<div class="flex flex-wrap gap-1">
|
|
90
|
+
<button type="button" aria-expanded={presetsOpen} class={imageChipClass(presetsOpen)} onclick={() => (presetsOpen = !presetsOpen)} title="Paint with a brush from the brush library">Brush presets{presetName ? `: ${presetName}` : ''}</button>
|
|
91
|
+
<button type="button" aria-expanded={editing} class={imageChipClass(editing)} onclick={() => (editing = !editing)} title="Edit this brush, or make a new one, in the shared brush library">Edit brush…</button>
|
|
92
|
+
</div>
|
|
93
|
+
{#if editing}
|
|
94
|
+
<BrushLibraryEditor
|
|
95
|
+
library={controller.brushLibrary}
|
|
96
|
+
brushId={view.brushPreset}
|
|
97
|
+
profile={IMAGE_PROFILE}
|
|
98
|
+
renderPreview={renderBrushPreview ?? controller.brushPreview ?? undefined}
|
|
99
|
+
class="rounded border border-border bg-slate-950 p-2"
|
|
100
|
+
onsaved={(b) => {
|
|
101
|
+
error = null;
|
|
102
|
+
controller.setBrushPreset(b.id).catch((err) => (error = userErrorMessage(err)));
|
|
103
|
+
}}
|
|
104
|
+
onclose={() => (editing = false)}
|
|
105
|
+
/>
|
|
106
|
+
{/if}
|
|
86
107
|
{#if presetsOpen}
|
|
87
108
|
<BrushLibraryBrowser
|
|
88
109
|
entries={presets.entries}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
2
|
+
import { type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
2
3
|
interface Props {
|
|
3
4
|
controller: ImageEditorController;
|
|
4
5
|
/** The row of paint tool buttons (default true); hide it when the host has its own toolbar. */
|
|
5
6
|
showTools?: boolean;
|
|
7
|
+
/** Draws the brush editor's preview stroke (default: the controller's `brushPreview` option). */
|
|
8
|
+
renderBrushPreview?: BrushPreviewRenderer;
|
|
6
9
|
class?: string;
|
|
7
10
|
}
|
|
8
11
|
declare const BrushOptionsBar: import("svelte").Component<Props, {}, "">;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { brushLoadColor, brushTakesPaint, downloadBytes, toggleStickerRef, DEFAULT_PAINT_GRADIENT, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
4
4
|
import { brushColourHex, PAINT_PROFILE, type Brush, type BrushLibraryEntry } from '@pixerate/editor/brushes';
|
|
5
5
|
import { untrack } from 'svelte';
|
|
6
|
-
import BrushEditor from '
|
|
6
|
+
import BrushEditor from '../brushes/BrushEditor.svelte';
|
|
7
7
|
import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
|
|
8
8
|
import PaintGradientEditor from './PaintGradientEditor.svelte';
|
|
9
9
|
import { attempt, button, chip, paintEditorState, panel } from './paintEditorRune.svelte.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
// Core's `SliderControl` (paint-editor), written out so the brush editor, which other entries
|
|
3
|
+
// use, does not reach the paint editor's entry (tests/entry-isolation.test.ts).
|
|
4
|
+
type SliderControl = { path: string; label: string; min: number; max: number; step: number };
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
5
7
|
control: SliderControl;
|
|
@@ -3,7 +3,7 @@ import type PaintEditorComponent from './PaintEditor.svelte';
|
|
|
3
3
|
import type PaintCanvasComponent from './PaintCanvas.svelte';
|
|
4
4
|
import type PaintToolbarComponent from './PaintToolbar.svelte';
|
|
5
5
|
import type BrushPanelComponent from './BrushPanel.svelte';
|
|
6
|
-
import type BrushEditorComponent from '
|
|
6
|
+
import type BrushEditorComponent from '../brushes/BrushEditor.svelte';
|
|
7
7
|
import type BrushLibraryBrowserComponent from '../brushes/BrushLibraryBrowser.svelte';
|
|
8
8
|
import type PaperPanelComponent from './PaperPanel.svelte';
|
|
9
9
|
import type ColorPanelComponent from './ColorPanel.svelte';
|
|
@@ -19,7 +19,7 @@ export { default as PaintEditor } from './PaintEditor.svelte';
|
|
|
19
19
|
export { default as PaintCanvas } from './PaintCanvas.svelte';
|
|
20
20
|
export { default as PaintToolbar } from './PaintToolbar.svelte';
|
|
21
21
|
export { default as BrushPanel } from './BrushPanel.svelte';
|
|
22
|
-
export { default as BrushEditor } from '
|
|
22
|
+
export { default as BrushEditor } from '../brushes/BrushEditor.svelte';
|
|
23
23
|
export { default as BrushLibraryBrowser } from '../brushes/BrushLibraryBrowser.svelte';
|
|
24
24
|
export { default as PaperPanel } from './PaperPanel.svelte';
|
|
25
25
|
export { default as ColorPanel } from './ColorPanel.svelte';
|
|
@@ -2,7 +2,8 @@ export { default as PaintEditor } from './PaintEditor.svelte';
|
|
|
2
2
|
export { default as PaintCanvas } from './PaintCanvas.svelte';
|
|
3
3
|
export { default as PaintToolbar } from './PaintToolbar.svelte';
|
|
4
4
|
export { default as BrushPanel } from './BrushPanel.svelte';
|
|
5
|
-
|
|
5
|
+
// The brush editor moved to `@pixerate/editor-svelte/brushes`; it is exported here too.
|
|
6
|
+
export { default as BrushEditor } from '../brushes/BrushEditor.svelte';
|
|
6
7
|
// The library browser moved to `@pixerate/editor-svelte/brushes`; it is exported here too.
|
|
7
8
|
export { default as BrushLibraryBrowser } from '../brushes/BrushLibraryBrowser.svelte';
|
|
8
9
|
export { default as PaperPanel } from './PaperPanel.svelte';
|
|
@@ -24,8 +24,7 @@ export declare function createPaintDirector(controller: PaintEditorController, o
|
|
|
24
24
|
presence: PaintPresence;
|
|
25
25
|
director: PaintDirector;
|
|
26
26
|
};
|
|
27
|
-
export
|
|
28
|
-
export declare const button = "px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700 disabled:opacity-40";
|
|
27
|
+
export { button, chip } from '../brushes/styles.js';
|
|
29
28
|
export declare const panel = "flex flex-col gap-2 text-xs text-slate-200";
|
|
30
29
|
/** Runs `fn` and returns the error message it threw (or a rejected promise's, through `onAsync`). */
|
|
31
30
|
export declare function attempt(fn: () => unknown, onError: (msg: string | null) => void): void;
|
|
@@ -57,8 +57,7 @@ export function createPaintDirector(controller, options = {}) {
|
|
|
57
57
|
const presence = new PaintPresence();
|
|
58
58
|
return { presence, director: new PaintDirector(controller, presence, options) };
|
|
59
59
|
}
|
|
60
|
-
export
|
|
61
|
-
export const button = 'px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700 disabled:opacity-40';
|
|
60
|
+
export { button, chip } from '../brushes/styles.js';
|
|
62
61
|
export const panel = 'flex flex-col gap-2 text-xs text-slate-200';
|
|
63
62
|
/** Runs `fn` and returns the error message it threw (or a rejected promise's, through `onAsync`). */
|
|
64
63
|
export function attempt(fn, onError) {
|
|
@@ -12,21 +12,35 @@
|
|
|
12
12
|
type VectorStrokeAlign,
|
|
13
13
|
randomVectorBrushstrokeSeed
|
|
14
14
|
} from '@pixerate/editor/vector';
|
|
15
|
+
import { isBrushLibrary, VECTOR_PROFILE, type Brush, type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
15
16
|
import { vectorStateOf } from './subscribe.svelte.js';
|
|
17
|
+
import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
|
|
18
|
+
import BrushLibraryEditor from '../brushes/BrushLibraryEditor.svelte';
|
|
19
|
+
import { brushLibraryState } from '../brushes/brushLibrary.svelte.js';
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* The appearance of the selection: its fill and stroke stacks (listed top first; add, remove,
|
|
19
23
|
* reorder, show / hide, paint, opacity and blend mode per layer, width and alignment per
|
|
20
24
|
* stroke), the node blend mode, and for one selected node its filters (drop and inner shadows,
|
|
21
|
-
* glows, blurs, feather). The pencil on a layer points the gradient editor and tool at it.
|
|
25
|
+
* glows, blurs, feather). The pencil on a layer points the gradient editor and tool at it. With
|
|
26
|
+
* the controller's `brushLibrary`, a brushstroke's "Brush…" picks from the library
|
|
27
|
+
* (`BrushLibraryBrowser`, `VECTOR_PROFILE`) and "Edit brush…" edits it (`BrushLibraryEditor`):
|
|
28
|
+
* saves go to the library and the stroke takes the saved brush.
|
|
22
29
|
* Slider drags and typing are one undo step each.
|
|
23
30
|
*/
|
|
24
31
|
interface Props {
|
|
25
32
|
controller: VectorEditorController;
|
|
33
|
+
/** The brush editor's preview stroke (default: the controller's `brushPreview` option). */
|
|
34
|
+
renderBrushPreview?: BrushPreviewRenderer;
|
|
26
35
|
class?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
let { controller, class: className = '' }: Props = $props();
|
|
38
|
+
let { controller, renderBrushPreview, class: className = '' }: Props = $props();
|
|
39
|
+
// The brushstroke whose brush is being picked or edited.
|
|
40
|
+
let brushUi = $state<{ index: number; mode: 'pick' | 'edit' } | null>(null);
|
|
41
|
+
const library = $derived(controller.brushLibrary);
|
|
42
|
+
const brushEntries = brushLibraryState(() => (brushUi ? controller.brushLibrary : null), () => ({ profile: VECTOR_PROFILE }));
|
|
43
|
+
const toggleBrushUi = (index: number, mode: 'pick' | 'edit') => (brushUi = brushUi?.index === index && brushUi.mode === mode ? null : { index, mode });
|
|
30
44
|
|
|
31
45
|
const store = vectorStateOf(() => controller);
|
|
32
46
|
const view = $derived(store.current);
|
|
@@ -85,6 +99,28 @@
|
|
|
85
99
|
</label>
|
|
86
100
|
<button type="button" class={buttonClass} title="Remove" aria-label="Remove {name}" onclick={() => controller.removePaintLayer('stroke', l.index, { ids })}>×</button>
|
|
87
101
|
</div>
|
|
102
|
+
{#if library}
|
|
103
|
+
<div class="flex gap-1">
|
|
104
|
+
<button type="button" class={buttonClass} aria-expanded={brushUi?.index === l.index && brushUi.mode === 'pick'} title="Pick a brush from the brush library" onclick={() => toggleBrushUi(l.index, 'pick')}>Brush…</button>
|
|
105
|
+
{#if isBrushLibrary(library)}
|
|
106
|
+
<button type="button" class={buttonClass} aria-expanded={brushUi?.index === l.index && brushUi.mode === 'edit'} title="Edit this brush, or make a new one, in the shared brush library" onclick={() => toggleBrushUi(l.index, 'edit')}>Edit brush…</button>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
{/if}
|
|
110
|
+
{#if brushUi?.index === l.index && brushUi.mode === 'pick'}
|
|
111
|
+
<BrushLibraryBrowser entries={brushEntries.entries} activeId={bs.brushId} profile={VECTOR_PROFILE} onselect={(id) => controller.setBrushstroke(l.index, { brushId: id }, { ids })} />
|
|
112
|
+
{/if}
|
|
113
|
+
{#if brushUi?.index === l.index && brushUi.mode === 'edit' && isBrushLibrary(library)}
|
|
114
|
+
<BrushLibraryEditor
|
|
115
|
+
{library}
|
|
116
|
+
brushId={bs.brushId}
|
|
117
|
+
profile={VECTOR_PROFILE}
|
|
118
|
+
renderPreview={renderBrushPreview ?? controller.brushPreview ?? undefined}
|
|
119
|
+
class="rounded border border-border bg-slate-950 p-2"
|
|
120
|
+
onsaved={(saved: Brush) => void controller.refreshBrushes().then(() => controller.setBrushstroke(l.index, { brushId: saved.id, keepColour: true }, { ids }))}
|
|
121
|
+
onclose={() => (brushUi = null)}
|
|
122
|
+
/>
|
|
123
|
+
{/if}
|
|
88
124
|
</div>
|
|
89
125
|
{:else if l.unknownKind}
|
|
90
126
|
<div class="text-[11px] text-muted-foreground" data-vector-layer="{l.kind}-{l.index}">{name}: {l.unknownKind} (not drawn by this version)</div>
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { type VectorEditorController } from '@pixerate/editor/vector';
|
|
2
|
+
import { type BrushPreviewRenderer } from '@pixerate/editor/brushes';
|
|
2
3
|
/**
|
|
3
4
|
* The appearance of the selection: its fill and stroke stacks (listed top first; add, remove,
|
|
4
5
|
* reorder, show / hide, paint, opacity and blend mode per layer, width and alignment per
|
|
5
6
|
* stroke), the node blend mode, and for one selected node its filters (drop and inner shadows,
|
|
6
|
-
* glows, blurs, feather). The pencil on a layer points the gradient editor and tool at it.
|
|
7
|
+
* glows, blurs, feather). The pencil on a layer points the gradient editor and tool at it. With
|
|
8
|
+
* the controller's `brushLibrary`, a brushstroke's "Brush…" picks from the library
|
|
9
|
+
* (`BrushLibraryBrowser`, `VECTOR_PROFILE`) and "Edit brush…" edits it (`BrushLibraryEditor`):
|
|
10
|
+
* saves go to the library and the stroke takes the saved brush.
|
|
7
11
|
* Slider drags and typing are one undo step each.
|
|
8
12
|
*/
|
|
9
13
|
interface Props {
|
|
10
14
|
controller: VectorEditorController;
|
|
15
|
+
/** The brush editor's preview stroke (default: the controller's `brushPreview` option). */
|
|
16
|
+
renderBrushPreview?: BrushPreviewRenderer;
|
|
11
17
|
class?: string;
|
|
12
18
|
}
|
|
13
19
|
declare const VectorAppearancePanel: import("svelte").Component<Props, {}, "">;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.195.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.264.0"
|
|
179
179
|
},
|
|
180
180
|
"peerDependencies": {
|
|
181
181
|
"@dagrejs/dagre": "^1.1.4",
|