@pixerate/editor-svelte 0.187.2 → 0.189.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}/BrushLibraryBrowser.svelte +4 -4
- package/dist/{paint-editor → brushes}/BrushLibraryBrowser.svelte.d.ts +2 -2
- package/dist/brushes/Thumbnail.svelte +26 -0
- package/dist/brushes/Thumbnail.svelte.d.ts +13 -0
- package/dist/brushes/brushLibrary.svelte.d.ts +9 -0
- package/dist/brushes/brushLibrary.svelte.js +27 -0
- package/dist/brushes/index.d.ts +5 -0
- package/dist/brushes/index.js +2 -0
- package/dist/canvas/createCanvasDraw.svelte.d.ts +1 -1
- package/dist/canvas/shapeBrushstroke.svelte.d.ts +7 -2
- package/dist/canvas/shapeBrushstroke.svelte.js +12 -4
- package/dist/image-editor/BrushOptionsBar.svelte +24 -0
- package/dist/paint-editor/BrushPanel.svelte +1 -1
- package/dist/paint-editor/PaintCanvas.svelte +6 -1
- package/dist/paint-editor/PaintCanvas.svelte.d.ts +3 -1
- package/dist/paint-editor/PaintEditor.svelte +5 -3
- package/dist/paint-editor/PaintEditor.svelte.d.ts +3 -1
- package/dist/paint-editor/PaintProposalGhosts.svelte +46 -0
- package/dist/paint-editor/PaintProposalGhosts.svelte.d.ts +10 -0
- package/dist/paint-editor/index.d.ts +4 -2
- package/dist/paint-editor/index.js +4 -1
- package/dist/paint-editor/paintProposalRune.svelte.d.ts +32 -0
- package/dist/paint-editor/paintProposalRune.svelte.js +45 -0
- package/dist/vector/VectorAppearancePanel.svelte +5 -4
- package/package.json +7 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { brushLibraryGroups, type BrushHostProfile, type BrushLibraryEntry } from '@pixerate/editor/brushes';
|
|
3
|
-
import
|
|
3
|
+
import Thumbnail from './Thumbnail.svelte';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
/** The library's brushes (built-in presets and saved ones), as facets. */
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
onselect: (id: string) => void;
|
|
11
11
|
/** Only brushes this host renders. */
|
|
12
12
|
profile?: BrushHostProfile;
|
|
13
|
-
/** A thumbnail for a brush (a test stroke). */
|
|
14
|
-
thumbnail?: (id: string) => { width: number; height: number; data: Uint8ClampedArray } | null;
|
|
13
|
+
/** A thumbnail for a brush (a test stroke): pixels, or an image URL. */
|
|
14
|
+
thumbnail?: (id: string) => { width: number; height: number; data: Uint8ClampedArray } | string | null | undefined;
|
|
15
15
|
thumbnailWidth?: number;
|
|
16
16
|
thumbnailHeight?: number;
|
|
17
17
|
class?: string;
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
onclick={() => onselect(e.brush.id)}
|
|
41
41
|
>
|
|
42
42
|
{#if thumbnail}
|
|
43
|
-
<
|
|
43
|
+
<Thumbnail image={thumbnail(e.brush.id) ?? null} label={`${e.brush.name} thumbnail`} style={`width: 100%; aspect-ratio: ${thumbnailWidth} / ${thumbnailHeight}`} />
|
|
44
44
|
{/if}
|
|
45
45
|
<span class="truncate text-[10px] text-slate-300">{e.brush.name}</span>
|
|
46
46
|
</button>
|
|
@@ -7,12 +7,12 @@ interface Props {
|
|
|
7
7
|
onselect: (id: string) => void;
|
|
8
8
|
/** Only brushes this host renders. */
|
|
9
9
|
profile?: BrushHostProfile;
|
|
10
|
-
/** A thumbnail for a brush (a test stroke). */
|
|
10
|
+
/** A thumbnail for a brush (a test stroke): pixels, or an image URL. */
|
|
11
11
|
thumbnail?: (id: string) => {
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
14
|
data: Uint8ClampedArray;
|
|
15
|
-
} | null;
|
|
15
|
+
} | string | null | undefined;
|
|
16
16
|
thumbnailWidth?: number;
|
|
17
17
|
thumbnailHeight?: number;
|
|
18
18
|
class?: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** RGBA pixels drawn on a canvas, or an image URL (e.g. a brushstroke swatch's data URL). */
|
|
4
|
+
image: { width: number; height: number; data: Uint8ClampedArray } | string | null;
|
|
5
|
+
label: string;
|
|
6
|
+
style?: string;
|
|
7
|
+
}
|
|
8
|
+
let { image, label, style = '' }: Props = $props();
|
|
9
|
+
let canvas = $state<HTMLCanvasElement | null>(null);
|
|
10
|
+
|
|
11
|
+
$effect(() => {
|
|
12
|
+
const ctx = canvas?.getContext('2d');
|
|
13
|
+
if (!canvas || !ctx || !image || typeof image === 'string' || typeof ctx.createImageData !== 'function') return;
|
|
14
|
+
canvas.width = image.width;
|
|
15
|
+
canvas.height = image.height;
|
|
16
|
+
const img = ctx.createImageData(image.width, image.height);
|
|
17
|
+
img.data.set(image.data);
|
|
18
|
+
ctx.putImageData(img, 0, 0);
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if typeof image === 'string'}
|
|
23
|
+
<img src={image} alt={label} class="rounded object-contain" {style} />
|
|
24
|
+
{:else}
|
|
25
|
+
<canvas bind:this={canvas} role="img" aria-label={label} class="rounded" {style} width={image?.width} height={image?.height}></canvas>
|
|
26
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** RGBA pixels drawn on a canvas, or an image URL (e.g. a brushstroke swatch's data URL). */
|
|
3
|
+
image: {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
data: Uint8ClampedArray;
|
|
7
|
+
} | string | null;
|
|
8
|
+
label: string;
|
|
9
|
+
style?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const Thumbnail: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type Thumbnail = ReturnType<typeof Thumbnail>;
|
|
13
|
+
export default Thumbnail;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BrushFilter, BrushLibrary, BrushLibraryEntry } from '@pixerate/editor/brushes';
|
|
2
|
+
/**
|
|
3
|
+
* The brushes of a brush library as a rune (`filter`: e.g. `{ profile: CANVAS_PROFILE }` for what
|
|
4
|
+
* a host can paint), read again after every change to the library: a brush saved in another
|
|
5
|
+
* editor sharing it shows up. Empty until the first read. Call it during component setup.
|
|
6
|
+
*/
|
|
7
|
+
export declare function brushLibraryState(library: () => Pick<BrushLibrary, 'list' | 'subscribe'> | null | undefined, filter?: () => BrushFilter): {
|
|
8
|
+
readonly entries: BrushLibraryEntry[];
|
|
9
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brushes of a brush library as a rune (`filter`: e.g. `{ profile: CANVAS_PROFILE }` for what
|
|
3
|
+
* a host can paint), read again after every change to the library: a brush saved in another
|
|
4
|
+
* editor sharing it shows up. Empty until the first read. Call it during component setup.
|
|
5
|
+
*/
|
|
6
|
+
export function brushLibraryState(library, filter = () => ({})) {
|
|
7
|
+
let entries = $state([]);
|
|
8
|
+
$effect(() => {
|
|
9
|
+
const lib = library();
|
|
10
|
+
const f = filter();
|
|
11
|
+
if (!lib)
|
|
12
|
+
return;
|
|
13
|
+
let live = true;
|
|
14
|
+
const read = () => void lib.list(f).then((list) => live && (entries = list), () => undefined);
|
|
15
|
+
read();
|
|
16
|
+
const unsubscribe = lib.subscribe(read);
|
|
17
|
+
return () => {
|
|
18
|
+
live = false;
|
|
19
|
+
unsubscribe();
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
get entries() {
|
|
24
|
+
return entries;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import type BrushLibraryBrowserComponent from './BrushLibraryBrowser.svelte';
|
|
3
|
+
export { default as BrushLibraryBrowser } from './BrushLibraryBrowser.svelte';
|
|
4
|
+
export { brushLibraryState } from './brushLibrary.svelte.js';
|
|
5
|
+
export type BrushLibraryBrowserProps = ComponentProps<typeof BrushLibraryBrowserComponent>;
|
|
@@ -33,7 +33,7 @@ export declare function createCanvasDraw<TNode extends CanvasNode = CanvasNode>(
|
|
|
33
33
|
height: number;
|
|
34
34
|
} | undefined;
|
|
35
35
|
/** The dry paint presets `style.brush` accepts (loads the paint engine). */
|
|
36
|
-
listBrushes:
|
|
36
|
+
listBrushes: () => Promise<import("@pixerate/editor/canvas").CanvasBrushPreset[]>;
|
|
37
37
|
};
|
|
38
38
|
export type CanvasDraw<TNode extends CanvasNode = CanvasNode> = ReturnType<typeof createCanvasDraw<TNode>>;
|
|
39
39
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CanvasBrushPreset, type CanvasBrushSwatchOptions } from '@pixerate/editor/canvas';
|
|
2
|
+
import type { BrushLibrary } from '@pixerate/editor/brushes';
|
|
2
3
|
/**
|
|
3
4
|
* Tells brushed `ShapeNode`s the canvas zoom so their paint stays sharp. Call it in the component
|
|
4
5
|
* that renders `<SvelteFlow>`, with a getter of the live zoom. Nodes re-render only when the zoom
|
|
@@ -8,15 +9,19 @@ export declare function setShapeNodeZoom(getZoom: () => number): void;
|
|
|
8
9
|
/** The bucketed canvas zoom for brushed shape nodes (1 when no `setShapeNodeZoom` is above). */
|
|
9
10
|
export declare function getShapeNodeZoom(): () => number;
|
|
10
11
|
/**
|
|
11
|
-
* The
|
|
12
|
-
* `
|
|
12
|
+
* The brushes a freehand stroke can be brushed with (loads the paint engine): the dry presets and,
|
|
13
|
+
* with `library`, its saved brushes the canvas can paint (each with its `spec`; pass it as the
|
|
14
|
+
* stroke style's `brushSpec`), read again when the library changes (`dispose` stops that). With
|
|
15
|
+
* `swatches`, each has a preview image (`renderCanvasBrushSwatch`) for a brush menu.
|
|
13
16
|
*/
|
|
14
17
|
export declare function createCanvasBrushes(options?: {
|
|
15
18
|
swatches?: CanvasBrushSwatchOptions;
|
|
19
|
+
library?: Pick<BrushLibrary, 'list' | 'subscribe'> | null;
|
|
16
20
|
}): {
|
|
17
21
|
readonly brushes: (CanvasBrushPreset & {
|
|
18
22
|
swatch?: string;
|
|
19
23
|
})[];
|
|
20
24
|
readonly loading: boolean;
|
|
21
25
|
readonly error: string | null;
|
|
26
|
+
dispose(): void;
|
|
22
27
|
};
|
|
@@ -14,16 +14,19 @@ export function getShapeNodeZoom() {
|
|
|
14
14
|
return getContext(ZOOM_KEY) ?? (() => 1);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The
|
|
18
|
-
* `
|
|
17
|
+
* The brushes a freehand stroke can be brushed with (loads the paint engine): the dry presets and,
|
|
18
|
+
* with `library`, its saved brushes the canvas can paint (each with its `spec`; pass it as the
|
|
19
|
+
* stroke style's `brushSpec`), read again when the library changes (`dispose` stops that). With
|
|
20
|
+
* `swatches`, each has a preview image (`renderCanvasBrushSwatch`) for a brush menu.
|
|
19
21
|
*/
|
|
20
22
|
export function createCanvasBrushes(options = {}) {
|
|
21
23
|
let brushes = $state.raw([]);
|
|
22
24
|
let loading = $state(true);
|
|
23
25
|
let error = $state(null);
|
|
24
26
|
const opts = options.swatches;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const library = options.library ?? undefined;
|
|
28
|
+
const read = () => listCanvasBrushes(library)
|
|
29
|
+
.then(async (list) => (opts ? Promise.all(list.map(async (b) => ({ ...b, swatch: await renderCanvasBrushSwatch(b.id, opts, b.spec) }))) : list))
|
|
27
30
|
.then((list) => {
|
|
28
31
|
brushes = list;
|
|
29
32
|
loading = false;
|
|
@@ -31,6 +34,8 @@ export function createCanvasBrushes(options = {}) {
|
|
|
31
34
|
error = e instanceof Error ? e.message : String(e);
|
|
32
35
|
loading = false;
|
|
33
36
|
});
|
|
37
|
+
void read();
|
|
38
|
+
const unsubscribe = library?.subscribe(() => void read());
|
|
34
39
|
return {
|
|
35
40
|
get brushes() {
|
|
36
41
|
return brushes;
|
|
@@ -40,6 +45,9 @@ export function createCanvasBrushes(options = {}) {
|
|
|
40
45
|
},
|
|
41
46
|
get error() {
|
|
42
47
|
return error;
|
|
48
|
+
},
|
|
49
|
+
dispose() {
|
|
50
|
+
unsubscribe?.();
|
|
43
51
|
}
|
|
44
52
|
};
|
|
45
53
|
}
|
|
@@ -24,6 +24,9 @@
|
|
|
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';
|
|
28
|
+
import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
|
|
29
|
+
import { brushLibraryState } from '../brushes/brushLibrary.svelte.js';
|
|
27
30
|
|
|
28
31
|
interface Props {
|
|
29
32
|
controller: ImageEditorController;
|
|
@@ -46,6 +49,10 @@
|
|
|
46
49
|
return { tool: s.activeTool, sourceLabel: cloneSourceLabel(controller), cloneView: cloneTransformView(controller), ...s.paint };
|
|
47
50
|
});
|
|
48
51
|
let error = $state<string | null>(null);
|
|
52
|
+
let presetsOpen = $state(false);
|
|
53
|
+
// Read only while open: the list follows the library, so a brush saved in another editor shows up.
|
|
54
|
+
const presets = brushLibraryState(() => (presetsOpen ? controller.brushLibrary : null), () => ({ profile: IMAGE_PROFILE }));
|
|
55
|
+
const presetName = $derived(presets.entries.find((e) => e.brush.id === view.brushPreset)?.brush.name ?? view.brushPreset);
|
|
49
56
|
|
|
50
57
|
|
|
51
58
|
|
|
@@ -73,6 +80,23 @@
|
|
|
73
80
|
</div>
|
|
74
81
|
{/if}
|
|
75
82
|
|
|
83
|
+
{#if view.tool === 'brush' || view.tool === 'mixer' || view.tool === 'eraser'}
|
|
84
|
+
<div class="flex flex-col gap-1" data-testid="brush-presets">
|
|
85
|
+
<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>
|
|
86
|
+
{#if presetsOpen}
|
|
87
|
+
<BrushLibraryBrowser
|
|
88
|
+
entries={presets.entries}
|
|
89
|
+
activeId={view.brushPreset ?? undefined}
|
|
90
|
+
profile={IMAGE_PROFILE}
|
|
91
|
+
onselect={(id) => {
|
|
92
|
+
error = null;
|
|
93
|
+
controller.setBrushPreset(id).catch((err) => (error = userErrorMessage(err)));
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
{/if}
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
99
|
+
|
|
76
100
|
<div class="flex items-center gap-2" role="group" aria-label="Colours">
|
|
77
101
|
<label class="flex items-center gap-1">
|
|
78
102
|
<ImageColor label="Foreground colour" value={view.foreground} oninput={(foreground) => controller.setPaintOptions({ foreground })} />
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { PAINT_PROFILE, type Brush, type BrushLibraryEntry } from '@pixerate/editor/brushes';
|
|
5
5
|
import { untrack } from 'svelte';
|
|
6
6
|
import BrushEditor from './BrushEditor.svelte';
|
|
7
|
-
import BrushLibraryBrowser from '
|
|
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';
|
|
10
10
|
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
viewCssTransform,
|
|
11
11
|
type PaintEditorController,
|
|
12
12
|
type PaintPresence,
|
|
13
|
+
type PaintProposals,
|
|
13
14
|
type PaintTool,
|
|
14
15
|
} from '@pixerate/editor/paint-editor';
|
|
15
16
|
import { untrack } from 'svelte';
|
|
16
17
|
import { paintEditorState } from './paintEditorRune.svelte.js';
|
|
17
18
|
import PaintPresenceLayer from './PaintPresenceLayer.svelte';
|
|
19
|
+
import PaintProposalGhosts from './PaintProposalGhosts.svelte';
|
|
18
20
|
import ReferencePanel from './ReferencePanel.svelte';
|
|
19
21
|
|
|
20
22
|
interface Props {
|
|
@@ -29,9 +31,11 @@
|
|
|
29
31
|
onerror?: (message: string | null) => void;
|
|
30
32
|
/** Agent cursors to draw over the canvas (see `PaintDirector` and `createPaintDirector`). */
|
|
31
33
|
presence?: PaintPresence;
|
|
34
|
+
/** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
|
|
35
|
+
proposals?: PaintProposals;
|
|
32
36
|
class?: string;
|
|
33
37
|
}
|
|
34
|
-
let { controller, tool = 'brush', showGuides = true, fitOnMount = true, onerror, presence, class: className = '' }: Props = $props();
|
|
38
|
+
let { controller, tool = 'brush', showGuides = true, fitOnMount = true, onerror, presence, proposals, class: className = '' }: Props = $props();
|
|
35
39
|
|
|
36
40
|
const editor = paintEditorState(() => controller);
|
|
37
41
|
const snap = $derived(editor.current);
|
|
@@ -201,6 +205,7 @@
|
|
|
201
205
|
{#each snap.guides.references.filter((r) => r.visible) as r (r.id)}
|
|
202
206
|
<ReferencePanel {controller} reference={r} />
|
|
203
207
|
{/each}
|
|
208
|
+
{#if proposals}<PaintProposalGhosts {proposals} {transform} zoom={snap.view.zoom} />{/if}
|
|
204
209
|
{#if presence}<PaintPresenceLayer {presence} view={snap.view} canvas={doc} viewport={size} />{/if}
|
|
205
210
|
<div class="absolute bottom-1 right-2 text-[10px] text-slate-500 pointer-events-none" data-testid="paint-view-status">
|
|
206
211
|
{Math.round(snap.view.zoom * 100)}% · {Math.round(snap.view.rotation)}°{snap.view.mirror ? ' · mirrored' : ''}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PaintEditorController, type PaintPresence, type PaintTool } from '@pixerate/editor/paint-editor';
|
|
1
|
+
import { type PaintEditorController, type PaintPresence, type PaintProposals, type PaintTool } from '@pixerate/editor/paint-editor';
|
|
2
2
|
interface Props {
|
|
3
3
|
controller: PaintEditorController;
|
|
4
4
|
/** The active tool (default brush). */
|
|
@@ -11,6 +11,8 @@ interface Props {
|
|
|
11
11
|
onerror?: (message: string | null) => void;
|
|
12
12
|
/** Agent cursors to draw over the canvas (see `PaintDirector` and `createPaintDirector`). */
|
|
13
13
|
presence?: PaintPresence;
|
|
14
|
+
/** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
|
|
15
|
+
proposals?: PaintProposals;
|
|
14
16
|
class?: string;
|
|
15
17
|
}
|
|
16
18
|
declare const PaintCanvas: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { PaintPresence, documentToScreen, screenToDocument, type PaintEditorController, type PaintTool } from '@pixerate/editor/paint-editor';
|
|
2
|
+
import { PaintPresence, documentToScreen, screenToDocument, type PaintEditorController, type PaintProposals, type PaintTool } from '@pixerate/editor/paint-editor';
|
|
3
3
|
import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
|
|
4
4
|
import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
|
|
5
5
|
import PresenceLayer from '../playback/PresenceLayer.svelte';
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
height?: number | string;
|
|
24
24
|
/** Agent cursors to draw over the canvas (see `createPaintDirector`). */
|
|
25
25
|
presence?: PaintPresence;
|
|
26
|
+
/** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
|
|
27
|
+
proposals?: PaintProposals;
|
|
26
28
|
class?: string;
|
|
27
29
|
/**
|
|
28
30
|
* Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
/** Bring the editor on screen (switch to its tab) before the agent acts. */
|
|
36
38
|
onplaybackreveal?: () => void | Promise<void>;
|
|
37
39
|
}
|
|
38
|
-
let { controller, fileName, height = 720, presence, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
|
|
40
|
+
let { controller, fileName, height = 720, presence, proposals, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
|
|
39
41
|
|
|
40
42
|
/** Controls a paint plan can point at, by `{ kind: 'control', id }`. */
|
|
41
43
|
const PAINT_CONTROLS: Record<string, string> = {
|
|
@@ -116,7 +118,7 @@
|
|
|
116
118
|
<BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
|
|
117
119
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
118
120
|
<!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
|
|
119
|
-
<PaintCanvas {controller} {tool} presence={playing ? undefined : presence} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
|
|
121
|
+
<PaintCanvas {controller} {tool} presence={playing ? undefined : presence} {proposals} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
|
|
120
122
|
{#if error}<p role="alert" class="mt-1 text-[11px] text-rose-400">{error}</p>{/if}
|
|
121
123
|
</div>
|
|
122
124
|
<div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PaintPresence, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
1
|
+
import { PaintPresence, type PaintEditorController, type PaintProposals } from '@pixerate/editor/paint-editor';
|
|
2
2
|
import type { AgentPlayback } from '@pixerate/editor/tools';
|
|
3
3
|
interface Props {
|
|
4
4
|
controller: PaintEditorController;
|
|
@@ -8,6 +8,8 @@ interface Props {
|
|
|
8
8
|
height?: number | string;
|
|
9
9
|
/** Agent cursors to draw over the canvas (see `createPaintDirector`). */
|
|
10
10
|
presence?: PaintPresence;
|
|
11
|
+
/** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
|
|
12
|
+
proposals?: PaintProposals;
|
|
11
13
|
class?: string;
|
|
12
14
|
/**
|
|
13
15
|
* Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { paintProposalGhost, type PaintProposals, type PaintProposalsState } from '@pixerate/editor/paint-editor';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
proposals: PaintProposals;
|
|
6
|
+
/** The view's CSS transform (`viewCssTransform`). */
|
|
7
|
+
transform: string;
|
|
8
|
+
zoom: number;
|
|
9
|
+
}
|
|
10
|
+
let { proposals, transform, zoom }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let pending = $state.raw<PaintProposalsState>({ proposals: [] });
|
|
13
|
+
$effect(() => {
|
|
14
|
+
pending = proposals.getState();
|
|
15
|
+
return proposals.subscribe((next) => {
|
|
16
|
+
pending = next;
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** Draws a ghost's pixels into its canvas. */
|
|
21
|
+
function ghost(node: HTMLCanvasElement, data: Uint8ClampedArray) {
|
|
22
|
+
const draw = (d: Uint8ClampedArray) => {
|
|
23
|
+
const ctx = node.getContext('2d');
|
|
24
|
+
if (ctx && typeof ImageData !== 'undefined') ctx.putImageData(new ImageData(new Uint8ClampedArray(d), node.width, node.height), 0, 0);
|
|
25
|
+
};
|
|
26
|
+
draw(data);
|
|
27
|
+
return { update: draw };
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!-- Pending paint proposals drawn over the canvas: each ghost where its paint would go, translucent with a dashed edge. -->
|
|
32
|
+
{#each pending.proposals as p (p.id)}
|
|
33
|
+
{@const g = paintProposalGhost(p)}
|
|
34
|
+
{#if g}
|
|
35
|
+
<canvas
|
|
36
|
+
use:ghost={g.data}
|
|
37
|
+
width={g.width}
|
|
38
|
+
height={g.height}
|
|
39
|
+
data-paint-proposal={p.id}
|
|
40
|
+
title={p.rationale ? `${p.title}: ${p.rationale}` : p.title}
|
|
41
|
+
aria-label={`Suggested: ${p.title}`}
|
|
42
|
+
class="absolute left-0 top-0 pointer-events-none"
|
|
43
|
+
style={`transform: ${transform} translate(${g.x}px, ${g.y}px); transform-origin: 0 0; opacity: 0.7; outline: ${1.5 / zoom}px dashed #a78bfa; outline-offset: ${2 / zoom}px`}
|
|
44
|
+
></canvas>
|
|
45
|
+
{/if}
|
|
46
|
+
{/each}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PaintProposals } from '@pixerate/editor/paint-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
proposals: PaintProposals;
|
|
4
|
+
/** The view's CSS transform (`viewCssTransform`). */
|
|
5
|
+
transform: string;
|
|
6
|
+
zoom: number;
|
|
7
|
+
}
|
|
8
|
+
declare const PaintProposalGhosts: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type PaintProposalGhosts = ReturnType<typeof PaintProposalGhosts>;
|
|
10
|
+
export default PaintProposalGhosts;
|
|
@@ -4,7 +4,7 @@ import type PaintCanvasComponent from './PaintCanvas.svelte';
|
|
|
4
4
|
import type PaintToolbarComponent from './PaintToolbar.svelte';
|
|
5
5
|
import type BrushPanelComponent from './BrushPanel.svelte';
|
|
6
6
|
import type BrushEditorComponent from './BrushEditor.svelte';
|
|
7
|
-
import type BrushLibraryBrowserComponent from '
|
|
7
|
+
import type BrushLibraryBrowserComponent from '../brushes/BrushLibraryBrowser.svelte';
|
|
8
8
|
import type PaperPanelComponent from './PaperPanel.svelte';
|
|
9
9
|
import type ColorPanelComponent from './ColorPanel.svelte';
|
|
10
10
|
import type PaintLayersPanelComponent from './PaintLayersPanel.svelte';
|
|
@@ -19,7 +19,7 @@ export { default as PaintCanvas } from './PaintCanvas.svelte';
|
|
|
19
19
|
export { default as PaintToolbar } from './PaintToolbar.svelte';
|
|
20
20
|
export { default as BrushPanel } from './BrushPanel.svelte';
|
|
21
21
|
export { default as BrushEditor } from './BrushEditor.svelte';
|
|
22
|
-
export { default as BrushLibraryBrowser } from '
|
|
22
|
+
export { default as BrushLibraryBrowser } from '../brushes/BrushLibraryBrowser.svelte';
|
|
23
23
|
export { default as PaperPanel } from './PaperPanel.svelte';
|
|
24
24
|
export { default as ColorPanel } from './ColorPanel.svelte';
|
|
25
25
|
export { default as PaintLayersPanel } from './PaintLayersPanel.svelte';
|
|
@@ -31,6 +31,8 @@ export { default as PaintEffectsPanel } from './PaintEffectsPanel.svelte';
|
|
|
31
31
|
export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
|
|
32
32
|
export { default as PaintSlider } from './PaintSlider.svelte';
|
|
33
33
|
export { default as PaintPresenceLayer } from './PaintPresenceLayer.svelte';
|
|
34
|
+
export { default as PaintProposalGhosts } from './PaintProposalGhosts.svelte';
|
|
35
|
+
export { createPaintChangeWatcher, createPaintProposalState, type PaintChangeWatcherRune, type PaintProposalStateRune } from './paintProposalRune.svelte.js';
|
|
34
36
|
export { createPaintDirector, createReactivePaintEditor, paintEditorState, paintEffectsProvider, usePaintEffectsTools } from './paintEditorRune.svelte.js';
|
|
35
37
|
export type PaintEditorProps = ComponentProps<typeof PaintEditorComponent>;
|
|
36
38
|
export type PaintCanvasProps = ComponentProps<typeof PaintCanvasComponent>;
|
|
@@ -3,7 +3,8 @@ 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
|
export { default as BrushEditor } from './BrushEditor.svelte';
|
|
6
|
-
|
|
6
|
+
// The library browser moved to `@pixerate/editor-svelte/brushes`; it is exported here too.
|
|
7
|
+
export { default as BrushLibraryBrowser } from '../brushes/BrushLibraryBrowser.svelte';
|
|
7
8
|
export { default as PaperPanel } from './PaperPanel.svelte';
|
|
8
9
|
export { default as ColorPanel } from './ColorPanel.svelte';
|
|
9
10
|
export { default as PaintLayersPanel } from './PaintLayersPanel.svelte';
|
|
@@ -15,4 +16,6 @@ export { default as PaintEffectsPanel } from './PaintEffectsPanel.svelte';
|
|
|
15
16
|
export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
|
|
16
17
|
export { default as PaintSlider } from './PaintSlider.svelte';
|
|
17
18
|
export { default as PaintPresenceLayer } from './PaintPresenceLayer.svelte';
|
|
19
|
+
export { default as PaintProposalGhosts } from './PaintProposalGhosts.svelte';
|
|
20
|
+
export { createPaintChangeWatcher, createPaintProposalState } from './paintProposalRune.svelte.js';
|
|
18
21
|
export { createPaintDirector, createReactivePaintEditor, paintEditorState, paintEffectsProvider, usePaintEffectsTools } from './paintEditorRune.svelte.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PaintChange, type PaintChangeFeed, type PaintEditorController, type PaintProposal, type PaintProposals, type PaintProposalsOptions } from '@pixerate/editor/paint-editor';
|
|
2
|
+
export interface PaintProposalStateRune {
|
|
3
|
+
/** The framework-free store (for `createPaintProposalToolProvider` and agents). */
|
|
4
|
+
readonly store: PaintProposals;
|
|
5
|
+
/** Pending proposals (reactive); pass `store` to `PaintCanvas` (`proposals`) to draw them as ghosts. */
|
|
6
|
+
readonly proposals: readonly PaintProposal[];
|
|
7
|
+
propose: PaintProposals['propose'];
|
|
8
|
+
accept: PaintProposals['accept'];
|
|
9
|
+
reject: PaintProposals['reject'];
|
|
10
|
+
clear: PaintProposals['clear'];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Paint an agent suggests without painting it, as a Svelte 5 rune: ghosts the user accepts
|
|
14
|
+
* (painted on a layer of their own as one undo step, made by the proposer) or dismisses. Pass
|
|
15
|
+
* `store` to `PaintCanvas` as `proposals` to draw the ghosts, and to
|
|
16
|
+
* `createPaintProposalToolProvider` for agents. The same API as React's `usePaintProposals`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createPaintProposalState(controller: PaintEditorController, options?: Omit<PaintProposalsOptions, 'controller'>): PaintProposalStateRune;
|
|
19
|
+
export interface PaintChangeWatcherRune {
|
|
20
|
+
readonly feed: PaintChangeFeed;
|
|
21
|
+
/** The latest change (reactive). */
|
|
22
|
+
readonly last: PaintChange | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What changed in the painting, as a Svelte 5 rune: strokes, fills and layers added, removed or
|
|
26
|
+
* changed, and the start and end of each stroke, with who made it. For agents that paint
|
|
27
|
+
* alongside the user. Call during component initialisation; the feed stops on unmount. The same
|
|
28
|
+
* API as React's `usePaintChangeFeed`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createPaintChangeWatcher(controller: PaintEditorController, options?: {
|
|
31
|
+
onChange?: (change: PaintChange) => void;
|
|
32
|
+
}): PaintChangeWatcherRune;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createPaintChangeFeed, createPaintProposals, } from '@pixerate/editor/paint-editor';
|
|
2
|
+
/**
|
|
3
|
+
* Paint an agent suggests without painting it, as a Svelte 5 rune: ghosts the user accepts
|
|
4
|
+
* (painted on a layer of their own as one undo step, made by the proposer) or dismisses. Pass
|
|
5
|
+
* `store` to `PaintCanvas` as `proposals` to draw the ghosts, and to
|
|
6
|
+
* `createPaintProposalToolProvider` for agents. The same API as React's `usePaintProposals`.
|
|
7
|
+
*/
|
|
8
|
+
export function createPaintProposalState(controller, options = {}) {
|
|
9
|
+
const store = createPaintProposals({ ...options, controller });
|
|
10
|
+
let state = $state.raw(store.getState());
|
|
11
|
+
store.subscribe((next) => {
|
|
12
|
+
state = next;
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
store,
|
|
16
|
+
get proposals() {
|
|
17
|
+
return state.proposals;
|
|
18
|
+
},
|
|
19
|
+
propose: store.propose,
|
|
20
|
+
accept: store.accept,
|
|
21
|
+
reject: store.reject,
|
|
22
|
+
clear: store.clear,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What changed in the painting, as a Svelte 5 rune: strokes, fills and layers added, removed or
|
|
27
|
+
* changed, and the start and end of each stroke, with who made it. For agents that paint
|
|
28
|
+
* alongside the user. Call during component initialisation; the feed stops on unmount. The same
|
|
29
|
+
* API as React's `usePaintChangeFeed`.
|
|
30
|
+
*/
|
|
31
|
+
export function createPaintChangeWatcher(controller, options = {}) {
|
|
32
|
+
const feed = createPaintChangeFeed(controller);
|
|
33
|
+
let last = $state.raw(null);
|
|
34
|
+
feed.subscribe((change) => {
|
|
35
|
+
last = change;
|
|
36
|
+
options.onChange?.(change);
|
|
37
|
+
});
|
|
38
|
+
$effect(() => () => feed.dispose());
|
|
39
|
+
return {
|
|
40
|
+
feed,
|
|
41
|
+
get last() {
|
|
42
|
+
return last;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
type VectorFilterType,
|
|
11
11
|
type VectorFiltersPanelItem,
|
|
12
12
|
type VectorStrokeAlign,
|
|
13
|
-
randomVectorBrushstrokeSeed
|
|
14
|
-
vectorBrushstrokeBrushes
|
|
13
|
+
randomVectorBrushstrokeSeed
|
|
15
14
|
} from '@pixerate/editor/vector';
|
|
16
15
|
import { vectorStateOf } from './subscribe.svelte.js';
|
|
17
16
|
|
|
@@ -32,6 +31,8 @@
|
|
|
32
31
|
const store = vectorStateOf(() => controller);
|
|
33
32
|
const view = $derived(store.current);
|
|
34
33
|
const panel = $derived(getVectorAppearancePanel(view));
|
|
34
|
+
// Follows the editor state, which changes when the brush library is read again.
|
|
35
|
+
const brushes = $derived.by(() => (void view, controller.listBrushstrokeBrushes()));
|
|
35
36
|
|
|
36
37
|
const buttonClass =
|
|
37
38
|
'inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40';
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
<input type="checkbox" aria-label="{name} visible" title="Show" checked={l.visible} onchange={(e) => update(l, { visible: e.currentTarget.checked })} />
|
|
73
74
|
<input type="color" aria-label="{name} colour" class="h-7 w-10 cursor-pointer rounded border border-border bg-background" value={l.color ?? '#000000'} oninput={(e) => controller.setBrushstroke(l.index, { color: e.currentTarget.value }, { ids, merge: true })} onblur={() => controller.endMergedEdit()} />
|
|
74
75
|
<select aria-label="{name} brush" class={selectClass} value={bs.brushId} onchange={(e) => controller.setBrushstroke(l.index, { brushId: e.currentTarget.value }, { ids })}>
|
|
75
|
-
{#each
|
|
76
|
+
{#each brushes as b (b.id)}<option value={b.id}>{b.name}</option>{/each}
|
|
76
77
|
</select>
|
|
77
78
|
<input type="number" min="0.5" step="0.5" aria-label="{name} brush size" title="Brush size" class={numberClass} value={bs.size} oninput={(e) => {
|
|
78
79
|
const n = parseFloat(e.currentTarget.value);
|
|
@@ -169,7 +170,7 @@
|
|
|
169
170
|
onclick={() => controller.addPaintLayer(kind, {}, { ids })}>+</button
|
|
170
171
|
>
|
|
171
172
|
{#if kind === 'stroke'}
|
|
172
|
-
<button type="button" class={buttonClass} title="Add a brushstroke (a natural-media brush along the path)" aria-label="Add brushstroke" data-vector-add-brushstroke="" disabled={!ids.length} onclick={() => controller.addBrushstroke({ brushId:
|
|
173
|
+
<button type="button" class={buttonClass} title="Add a brushstroke (a natural-media brush along the path)" aria-label="Add brushstroke" data-vector-add-brushstroke="" disabled={!ids.length} onclick={() => controller.addBrushstroke({ brushId: brushes[0].id, seed: randomVectorBrushstrokeSeed() }, { ids })}>✎+</button>
|
|
173
174
|
{/if}
|
|
174
175
|
</div>
|
|
175
176
|
<!-- Top layer first, as it draws. -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.189.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
"svelte": "./dist/paint-editor/index.js",
|
|
49
49
|
"default": "./dist/paint-editor/index.js"
|
|
50
50
|
},
|
|
51
|
+
"./brushes": {
|
|
52
|
+
"types": "./dist/brushes/index.d.ts",
|
|
53
|
+
"svelte": "./dist/brushes/index.js",
|
|
54
|
+
"default": "./dist/brushes/index.js"
|
|
55
|
+
},
|
|
51
56
|
"./commands": {
|
|
52
57
|
"types": "./dist/commands/index.d.ts",
|
|
53
58
|
"svelte": "./dist/commands/index.js",
|
|
@@ -170,7 +175,7 @@
|
|
|
170
175
|
"**/*.css"
|
|
171
176
|
],
|
|
172
177
|
"dependencies": {
|
|
173
|
-
"@pixerate/editor": "0.
|
|
178
|
+
"@pixerate/editor": "0.256.0"
|
|
174
179
|
},
|
|
175
180
|
"peerDependencies": {
|
|
176
181
|
"@dagrejs/dagre": "^1.1.4",
|