@pixerate/editor-svelte 0.204.0 → 0.205.1
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/BrushPanel.svelte +15 -10
- package/dist/paint-editor/PaintCanvas.svelte +11 -1
- package/dist/paint-editor/PaintCanvas.svelte.d.ts +2 -0
- package/dist/paint-editor/PaintEditor.svelte +4 -2
- package/dist/paint-editor/PaintEditor.svelte.d.ts +2 -0
- package/dist/paint-editor/PaintLayersPanel.svelte +1 -1
- package/dist/paint-editor/PaperPanel.svelte +41 -4
- package/dist/paint-editor/index.d.ts +1 -1
- package/dist/paint-editor/index.js +1 -1
- package/dist/paint-editor/paintEditorRune.svelte.js +3 -1
- package/dist/paint-editor/paintProposalRune.svelte.d.ts +16 -1
- package/dist/paint-editor/paintProposalRune.svelte.js +36 -0
- package/package.json +2 -2
|
@@ -56,22 +56,27 @@
|
|
|
56
56
|
const carried = $derived(brushLoadColor(snap.mixerLoad));
|
|
57
57
|
// The brush's suggested colours, resolved for the colour scheme: one click makes one the paint colour.
|
|
58
58
|
const palette = $derived((spec.colour?.palette ?? []).flatMap((c) => brushColourHex(c, snap.colorScheme) ?? []));
|
|
59
|
-
const loads = $derived((['dab', 'mixer', 'impasto'].includes(brush.engine) || (brush.engine === 'wet' && brush.wet.tool === 'paint')) && !brush.ghost);
|
|
59
|
+
const loads = $derived((['dab', 'mixer', 'impasto'].includes(brush.engine) || (brush.engine === 'wet' && brush.wet.tool === 'paint')) && !brush.ghost && !brush.mutation);
|
|
60
60
|
const slug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'brush';
|
|
61
61
|
</script>
|
|
62
62
|
|
|
63
63
|
{#snippet extra(section: string)}
|
|
64
64
|
{#if section === 'shape' && brush.engine === 'sticker'}
|
|
65
65
|
<StickerPicker {controller} label="Sticker stamps" selected={brush.sticker.stamps} onpick={(ref) => controller.setBrush({ sticker: { stamps: toggleStickerRef(brush.sticker.stamps, ref) } })} />
|
|
66
|
-
{:else if section === 'behaviour'
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
{:else if section === 'behaviour'}
|
|
67
|
+
{#if brush.mutation && !snap.hasMutationAgent}
|
|
68
|
+
<p class="text-[11px] text-amber-400" role="status" data-paint-mutation-unconnected>No agent is connected — mutation strokes will stay as ordinary paint.</p>
|
|
69
|
+
{/if}
|
|
70
|
+
{#if loads}
|
|
71
|
+
<div class="flex items-center gap-1">
|
|
72
|
+
<span class="text-slate-400">Carries</span>
|
|
73
|
+
<span aria-label="Brush load" title={carried ?? 'Clean'} class="h-4 w-4 rounded ring-1 ring-slate-700" style={`background: ${carried ?? 'transparent'}`}></span>
|
|
74
|
+
{#if brush.engine === 'mixer'}
|
|
75
|
+
<button type="button" class={button} onclick={() => controller.loadMixerBrush()}>Load</button>
|
|
76
|
+
{/if}
|
|
77
|
+
<button type="button" class={button} onclick={() => controller.cleanMixerBrush(brush.engine === 'mixer')}>Clean</button>
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
75
80
|
{:else if section === 'colour'}
|
|
76
81
|
{#if palette.length}
|
|
77
82
|
<div class="flex flex-wrap items-center gap-1" role="group" aria-label="Paint with a palette colour">
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
PaintGesture,
|
|
5
5
|
drawWaterOverlay,
|
|
6
6
|
fitZoom,
|
|
7
|
+
startDeviceTilt,
|
|
7
8
|
startWetClock,
|
|
8
9
|
guideOverlay,
|
|
9
10
|
handlePaintShortcut,
|
|
@@ -41,9 +42,11 @@
|
|
|
41
42
|
proposals?: PaintProposals;
|
|
42
43
|
/** Strokes an agent is painting live alongside the user's, drawn as soft lines (see `createPaintAgentStrokes`). */
|
|
43
44
|
agentStrokes?: PaintAgentStrokes;
|
|
45
|
+
/** Follow device orientation (gyroscope tilt on iPhone and iPad) for thin-film iridescence view tilt. */
|
|
46
|
+
deviceTilt?: boolean;
|
|
44
47
|
class?: string;
|
|
45
48
|
}
|
|
46
|
-
let { controller, tool = 'brush', fillOptions, showGuides = true, fitOnMount = true, onerror, presence, proposals, agentStrokes, class: className = '' }: Props = $props();
|
|
49
|
+
let { controller, tool = 'brush', fillOptions, showGuides = true, fitOnMount = true, onerror, presence, proposals, agentStrokes, deviceTilt = false, class: className = '' }: Props = $props();
|
|
47
50
|
let agentLines = $state.raw<PaintAgentStrokesState>({ strokes: [] });
|
|
48
51
|
$effect(() => {
|
|
49
52
|
const store = agentStrokes;
|
|
@@ -118,6 +121,13 @@
|
|
|
118
121
|
return untrack(() => startWetClock(c));
|
|
119
122
|
});
|
|
120
123
|
|
|
124
|
+
// Device orientation shifts viewTilt when opted in.
|
|
125
|
+
$effect(() => {
|
|
126
|
+
const c = controller;
|
|
127
|
+
const on = deviceTilt;
|
|
128
|
+
return on ? untrack(() => startDeviceTilt(c)) : undefined;
|
|
129
|
+
});
|
|
130
|
+
|
|
121
131
|
// The water overlay.
|
|
122
132
|
$effect(() => {
|
|
123
133
|
void snap;
|
|
@@ -17,6 +17,8 @@ interface Props {
|
|
|
17
17
|
proposals?: PaintProposals;
|
|
18
18
|
/** Strokes an agent is painting live alongside the user's, drawn as soft lines (see `createPaintAgentStrokes`). */
|
|
19
19
|
agentStrokes?: PaintAgentStrokes;
|
|
20
|
+
/** Follow device orientation (gyroscope tilt on iPhone and iPad) for thin-film iridescence view tilt. */
|
|
21
|
+
deviceTilt?: boolean;
|
|
20
22
|
class?: string;
|
|
21
23
|
}
|
|
22
24
|
declare const PaintCanvas: import("svelte").Component<Props, {}, "">;
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
proposals?: PaintProposals;
|
|
30
30
|
/** Strokes an agent is painting live alongside the user's (see `createPaintAgentStrokes`). */
|
|
31
31
|
agentStrokes?: PaintAgentStrokes;
|
|
32
|
+
/** Follow device orientation (gyroscope tilt on iPhone and iPad) for thin-film iridescence view tilt. */
|
|
33
|
+
deviceTilt?: boolean;
|
|
32
34
|
class?: string;
|
|
33
35
|
/**
|
|
34
36
|
* Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
/** Bring the editor on screen (switch to its tab) before the agent acts. */
|
|
42
44
|
onplaybackreveal?: () => void | Promise<void>;
|
|
43
45
|
}
|
|
44
|
-
let { controller, fileName, height = 720, presence, proposals, agentStrokes, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
|
|
46
|
+
let { controller, fileName, height = 720, presence, proposals, agentStrokes, deviceTilt, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
|
|
45
47
|
|
|
46
48
|
/** Controls a paint plan can point at, by `{ kind: 'control', id }`. */
|
|
47
49
|
const PAINT_CONTROLS: Record<string, string> = {
|
|
@@ -123,7 +125,7 @@
|
|
|
123
125
|
<BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
|
|
124
126
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
125
127
|
<!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
|
|
126
|
-
<PaintCanvas {controller} {tool} {fillOptions} presence={playing ? undefined : presence} {proposals} {agentStrokes} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
|
|
128
|
+
<PaintCanvas {controller} {tool} {fillOptions} presence={playing ? undefined : presence} {proposals} {agentStrokes} {deviceTilt} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
|
|
127
129
|
{#if error}<p role="alert" class="mt-1 text-[11px] text-rose-400">{error}</p>{/if}
|
|
128
130
|
</div>
|
|
129
131
|
<div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
|
|
@@ -12,6 +12,8 @@ interface Props {
|
|
|
12
12
|
proposals?: PaintProposals;
|
|
13
13
|
/** Strokes an agent is painting live alongside the user's (see `createPaintAgentStrokes`). */
|
|
14
14
|
agentStrokes?: PaintAgentStrokes;
|
|
15
|
+
/** Follow device orientation (gyroscope tilt on iPhone and iPad) for thin-film iridescence view tilt. */
|
|
16
|
+
deviceTilt?: boolean;
|
|
15
17
|
class?: string;
|
|
16
18
|
/**
|
|
17
19
|
* Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const editor = paintEditorState(() => controller);
|
|
13
13
|
const layers = $derived.by(() => {
|
|
14
14
|
void editor.current;
|
|
15
|
-
return untrack(() => controller.listLayers());
|
|
15
|
+
return untrack(() => controller.listLayers().filter((l) => !l.temporary));
|
|
16
16
|
});
|
|
17
17
|
const active = $derived(layers.find((l) => l.active) ?? null);
|
|
18
18
|
const light = $derived.by(() => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { DEFAULT_BACKLIGHT_COLOR, LIGHT_SLIDERS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_SLIDERS, patchPath, readPath, renderPaper, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
2
|
+
import { DEFAULT_BACKLIGHT_COLOR, LIGHT_SLIDERS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_SLIDERS, UV_LIGHT_SLIDERS, VIEW_TILT_SLIDERS, patchPath, readPath, renderPaper, requestDeviceTiltPermission, startDeviceTilt, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
3
4
|
import PaintSlider from './PaintSlider.svelte';
|
|
4
5
|
import PixelCanvas from './PixelCanvas.svelte';
|
|
5
|
-
import { attempt, button, paintEditorState, panel } from './paintEditorRune.svelte.js';
|
|
6
|
+
import { attempt, button, chip, paintEditorState, panel } from './paintEditorRune.svelte.js';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
controller: PaintEditorController;
|
|
@@ -18,6 +19,13 @@
|
|
|
18
19
|
void editor.current;
|
|
19
20
|
return controller.getLighting();
|
|
20
21
|
});
|
|
22
|
+
const viewTilt = $derived(editor.current.viewTilt);
|
|
23
|
+
let tiltFollow = $state(false);
|
|
24
|
+
$effect(() => {
|
|
25
|
+
const c = controller;
|
|
26
|
+
const on = tiltFollow;
|
|
27
|
+
return on ? untrack(() => startDeviceTilt(c)) : undefined;
|
|
28
|
+
});
|
|
21
29
|
const previews = new Map(PAPER_LIBRARY.map((p) => [p.preset, { ...SWATCH, data: renderPaper({ ...p, shading: Math.max(0.8, p.shading) }, SWATCH) }]));
|
|
22
30
|
</script>
|
|
23
31
|
|
|
@@ -59,11 +67,40 @@
|
|
|
59
67
|
</label>
|
|
60
68
|
<button type="button" class={`self-start ${button}`} onclick={() => run(() => controller.setPaper({ seed: paper.seed + 1 }))}>New sheet</button>
|
|
61
69
|
<div class="flex flex-col gap-1.5" data-testid="light-panel">
|
|
62
|
-
<span class="font-medium text-slate-300">Light (thick paint, metallic paper)</span>
|
|
70
|
+
<span class="font-medium text-slate-300">Light (thick paint, iridescence, metallic paper)</span>
|
|
63
71
|
{#each LIGHT_SLIDERS as c (c.path)}
|
|
64
72
|
<PaintSlider control={c} value={readPath(lighting, c.path)} onchange={(v) => run(() => controller.setLighting({ [c.path]: v }))} />
|
|
65
73
|
{/each}
|
|
66
|
-
|
|
74
|
+
{#each VIEW_TILT_SLIDERS as c (c.path)}
|
|
75
|
+
<PaintSlider control={c} value={readPath(viewTilt, c.path)} onchange={(v) => run(() => controller.setViewTilt({ [c.path]: v }))} />
|
|
76
|
+
{/each}
|
|
77
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
aria-pressed={tiltFollow}
|
|
81
|
+
class={chip(tiltFollow)}
|
|
82
|
+
onclick={() =>
|
|
83
|
+
run(async () => {
|
|
84
|
+
if (!tiltFollow && !(await requestDeviceTiltPermission())) return;
|
|
85
|
+
tiltFollow = !tiltFollow;
|
|
86
|
+
})}
|
|
87
|
+
>Device tilt</button>
|
|
88
|
+
<button type="button" class={`self-start ${button}`} onclick={() => run(() => controller.flattenRelief())}>Flatten relief</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="flex flex-col gap-1.5" data-testid="uv-heat-panel">
|
|
92
|
+
<span class="font-medium text-slate-300">UV blacklight & heat</span>
|
|
93
|
+
<label class="flex items-center gap-2">
|
|
94
|
+
<input type="checkbox" checked={editor.current.uvLight.on} onchange={(e) => run(() => controller.setUvLight({ on: e.currentTarget.checked }))} /> UV blacklight
|
|
95
|
+
</label>
|
|
96
|
+
{#each UV_LIGHT_SLIDERS as c (c.path)}
|
|
97
|
+
<PaintSlider control={c} value={readPath(editor.current.uvLight, c.path)} onchange={(v) => run(() => controller.setUvLight({ [c.path]: v }))} />
|
|
98
|
+
{/each}
|
|
99
|
+
<div class="flex items-center gap-1.5">
|
|
100
|
+
<button type="button" class={button} onclick={() => run(() => controller.advanceHeat(10))}>Step heat</button>
|
|
101
|
+
<button type="button" class={button} onclick={() => run(() => controller.clearHeat())}>Cool canvas</button>
|
|
102
|
+
{#if editor.current.heat.hotCells > 0}<span class="text-[11px] text-amber-400">{Math.round(editor.current.heat.max * 100)}% warm</span>{/if}
|
|
103
|
+
</div>
|
|
67
104
|
</div>
|
|
68
105
|
{#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
|
|
69
106
|
</div>
|
|
@@ -36,7 +36,7 @@ export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
|
|
|
36
36
|
export { default as PaintSlider } from './PaintSlider.svelte';
|
|
37
37
|
export { default as PaintPresenceLayer } from './PaintPresenceLayer.svelte';
|
|
38
38
|
export { default as PaintProposalGhosts } from './PaintProposalGhosts.svelte';
|
|
39
|
-
export { createPaintChangeWatcher, createPaintProposalState, type PaintChangeWatcherRune, type PaintProposalStateRune } from './paintProposalRune.svelte.js';
|
|
39
|
+
export { createMutationRequests, createPaintChangeWatcher, createPaintProposalState, type PaintChangeWatcherRune, type PaintMutationRequestsRune, type PaintProposalStateRune, } from './paintProposalRune.svelte.js';
|
|
40
40
|
export { createPaintDirector, createReactivePaintEditor, paintEditorState, paintEffectsProvider, usePaintEffectsTools } from './paintEditorRune.svelte.js';
|
|
41
41
|
export type PaintEditorProps = ComponentProps<typeof PaintEditorComponent>;
|
|
42
42
|
export type PaintCanvasProps = ComponentProps<typeof PaintCanvasComponent>;
|
|
@@ -20,5 +20,5 @@ export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
|
|
|
20
20
|
export { default as PaintSlider } from './PaintSlider.svelte';
|
|
21
21
|
export { default as PaintPresenceLayer } from './PaintPresenceLayer.svelte';
|
|
22
22
|
export { default as PaintProposalGhosts } from './PaintProposalGhosts.svelte';
|
|
23
|
-
export { createPaintChangeWatcher, createPaintProposalState } from './paintProposalRune.svelte.js';
|
|
23
|
+
export { createMutationRequests, createPaintChangeWatcher, createPaintProposalState, } from './paintProposalRune.svelte.js';
|
|
24
24
|
export { createPaintDirector, createReactivePaintEditor, paintEditorState, paintEffectsProvider, usePaintEffectsTools } from './paintEditorRune.svelte.js';
|
|
@@ -55,7 +55,9 @@ export function paintEditorState(controller) {
|
|
|
55
55
|
*/
|
|
56
56
|
export function createPaintDirector(controller, options = {}) {
|
|
57
57
|
const presence = new PaintPresence();
|
|
58
|
-
|
|
58
|
+
const director = new PaintDirector(controller, presence, options);
|
|
59
|
+
controller.setMutationDirector(director);
|
|
60
|
+
return { presence, director };
|
|
59
61
|
}
|
|
60
62
|
export { button, chip } from '../brushes/styles.js';
|
|
61
63
|
export const panel = 'flex flex-col gap-2 text-xs text-slate-200';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PaintChange, type PaintChangeFeed, type PaintEditorController, type PaintProposal, type PaintProposals, type PaintProposalsOptions } from '@pixerate/editor/paint-editor';
|
|
1
|
+
import { type PaintChange, type PaintChangeFeed, type PaintEditorController, type PaintMutationInfo, type PaintMutationRequestListener, type PaintProposal, type PaintProposals, type PaintProposalsOptions } from '@pixerate/editor/paint-editor';
|
|
2
2
|
export interface PaintProposalStateRune {
|
|
3
3
|
/** The framework-free store (for `createPaintProposalToolProvider` and agents). */
|
|
4
4
|
readonly store: PaintProposals;
|
|
@@ -30,3 +30,18 @@ export interface PaintChangeWatcherRune {
|
|
|
30
30
|
export declare function createPaintChangeWatcher(controller: PaintEditorController, options?: {
|
|
31
31
|
onChange?: (change: PaintChange) => void;
|
|
32
32
|
}): PaintChangeWatcherRune;
|
|
33
|
+
export interface PaintMutationRequestsRune {
|
|
34
|
+
/** Pending mutations waiting for an agent to claim them (`state === 'pending'`). */
|
|
35
|
+
readonly pending: readonly PaintMutationInfo[];
|
|
36
|
+
/** Every active or completed mutation in the session. */
|
|
37
|
+
readonly mutations: readonly PaintMutationInfo[];
|
|
38
|
+
/** The most recently raised pending mutation, or `null`. */
|
|
39
|
+
readonly last: PaintMutationInfo | null;
|
|
40
|
+
destroy(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Subscribes to mutation requests raised when a user finishes a stroke with a `mutation` brush
|
|
44
|
+
* and pauses for `settle` seconds, as a Svelte 5 rune. While active, marks an agent listener as
|
|
45
|
+
* connected on the controller. The same API as React's `useMutationRequests`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createMutationRequests(controller: PaintEditorController, onRequest?: PaintMutationRequestListener): PaintMutationRequestsRune;
|
|
@@ -43,3 +43,39 @@ export function createPaintChangeWatcher(controller, options = {}) {
|
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Subscribes to mutation requests raised when a user finishes a stroke with a `mutation` brush
|
|
48
|
+
* and pauses for `settle` seconds, as a Svelte 5 rune. While active, marks an agent listener as
|
|
49
|
+
* connected on the controller. The same API as React's `useMutationRequests`.
|
|
50
|
+
*/
|
|
51
|
+
export function createMutationRequests(controller, onRequest) {
|
|
52
|
+
let mutations = $state.raw(controller.listMutations({ includeAll: true }));
|
|
53
|
+
let last = $state.raw(null);
|
|
54
|
+
const offState = controller.subscribe(() => {
|
|
55
|
+
mutations = controller.listMutations({ includeAll: true });
|
|
56
|
+
});
|
|
57
|
+
const offReq = controller.onMutationRequest((m) => {
|
|
58
|
+
last = m;
|
|
59
|
+
onRequest?.(m);
|
|
60
|
+
});
|
|
61
|
+
$effect(() => () => {
|
|
62
|
+
offState();
|
|
63
|
+
offReq();
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
get pending() {
|
|
67
|
+
return mutations.filter((m) => m.state === 'pending');
|
|
68
|
+
},
|
|
69
|
+
get mutations() {
|
|
70
|
+
return mutations;
|
|
71
|
+
},
|
|
72
|
+
get last() {
|
|
73
|
+
const p = mutations.filter((m) => m.state === 'pending');
|
|
74
|
+
return last && p.some((m) => m.id === last.id) ? last : (p[p.length - 1] ?? null);
|
|
75
|
+
},
|
|
76
|
+
destroy() {
|
|
77
|
+
offState();
|
|
78
|
+
offReq();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.205.1",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"**/*.css"
|
|
185
185
|
],
|
|
186
186
|
"dependencies": {
|
|
187
|
-
"@pixerate/editor": "0.
|
|
187
|
+
"@pixerate/editor": "0.275.1"
|
|
188
188
|
},
|
|
189
189
|
"peerDependencies": {
|
|
190
190
|
"@dagrejs/dagre": "^1.1.4",
|