@pixerate/editor-svelte 0.203.0 → 0.205.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.
@@ -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' && loads}
67
- <div class="flex items-center gap-1">
68
- <span class="text-slate-400">Carries</span>
69
- <span aria-label="Brush load" title={carried ?? 'Clean'} class="h-4 w-4 rounded ring-1 ring-slate-700" style={`background: ${carried ?? 'transparent'}`}></span>
70
- {#if brush.engine === 'mixer'}
71
- <button type="button" class={button} onclick={() => controller.loadMixerBrush()}>Load</button>
72
- {/if}
73
- <button type="button" class={button} onclick={() => controller.cleanMixerBrush(brush.engine === 'mixer')}>Clean</button>
74
- </div>
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">
@@ -0,0 +1,210 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import {
4
+ EVOLUTION_SLIDERS,
5
+ exportProposalContactSheetPng,
6
+ liveStrokeRefusal,
7
+ normalizeMutationRates,
8
+ paintProposalGhost,
9
+ proposalGenomeStrokes,
10
+ type EvolutionMutationRates,
11
+ type PaintEditorController,
12
+ type PaintProposals,
13
+ type PaintProposalsState,
14
+ } from '@pixerate/editor/paint-editor';
15
+ import PaintSlider from './PaintSlider.svelte';
16
+ import { attempt, button, chip, paintEditorState, panel } from './paintEditorRune.svelte.js';
17
+
18
+ interface Props {
19
+ controller: PaintEditorController;
20
+ /** Optional proposals store (defaults to `controller.proposals`). */
21
+ proposals?: PaintProposals;
22
+ class?: string;
23
+ }
24
+ let { controller, proposals, class: className = '' }: Props = $props();
25
+ const editor = paintEditorState(() => controller);
26
+ const store = $derived(proposals ?? controller.proposals);
27
+
28
+ let proposalState = $state.raw<PaintProposalsState>({ proposals: [] });
29
+ $effect(() => {
30
+ const s = store;
31
+ return untrack(() => {
32
+ proposalState = s.getState();
33
+ return s.subscribe((next) => {
34
+ proposalState = next;
35
+ });
36
+ });
37
+ });
38
+
39
+ let selectedStrokes = $state<string[]>([]);
40
+ let selectedProposals = $state<string[]>([]);
41
+ let count = $state(4);
42
+ let seed = $state(1);
43
+ let mutation = $state<Required<EvolutionMutationRates>>(normalizeMutationRates());
44
+ let contactSheetUrl = $state<string | null>(null);
45
+ let error = $state<string | null>(null);
46
+ const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
47
+
48
+ const eligibleStrokes = $derived(editor.current.document.strokes.filter((s) => !liveStrokeRefusal(s.brush)));
49
+ const activeStrokeIds = $derived(selectedStrokes.filter((id) => eligibleStrokes.some((s) => s.id === id)));
50
+ const effectiveStrokeIds = $derived(
51
+ activeStrokeIds.length > 0
52
+ ? activeStrokeIds
53
+ : editor.current.selectedStroke && eligibleStrokes.some((s) => s.id === editor.current.selectedStroke)
54
+ ? [editor.current.selectedStroke]
55
+ : eligibleStrokes.length > 0
56
+ ? [eligibleStrokes[eligibleStrokes.length - 1]!.id]
57
+ : []
58
+ );
59
+ const variants = $derived(proposalState.proposals.filter((p) => proposalGenomeStrokes(p).length > 0));
60
+ const activeProposalIds = $derived(selectedProposals.filter((id) => variants.some((v) => v.id === id)));
61
+
62
+ function toggleStroke(id: string) {
63
+ selectedStrokes = selectedStrokes.includes(id) ? selectedStrokes.filter((x) => x !== id) : [...selectedStrokes.slice(-15), id];
64
+ }
65
+
66
+ function toggleProposal(id: string) {
67
+ selectedProposals = selectedProposals.includes(id) ? selectedProposals.filter((x) => x !== id) : [...selectedProposals.slice(-1), id];
68
+ }
69
+
70
+ function drawPixels(node: HTMLCanvasElement, data: Uint8ClampedArray) {
71
+ const draw = (d: Uint8ClampedArray) => {
72
+ const ctx = node.getContext('2d');
73
+ if (ctx && typeof ImageData !== 'undefined') ctx.putImageData(new ImageData(new Uint8ClampedArray(d), node.width, node.height), 0, 0);
74
+ };
75
+ draw(data);
76
+ return { update: draw };
77
+ }
78
+ </script>
79
+
80
+ <!-- Evolutionary strokes: pick recorded dry strokes, adjust mutation rates, and breed a grid of mutated stroke proposals. -->
81
+ <div class={`${panel} ${className}`} data-testid="evolution-panel">
82
+ <div class="flex items-center justify-between">
83
+ <span class="font-medium text-slate-300">Evolution ({eligibleStrokes.length})</span>
84
+ <label class="flex items-center gap-1 text-[11px] text-slate-400">
85
+ <span>Count</span>
86
+ <input
87
+ type="number"
88
+ aria-label="Variant count"
89
+ min="2"
90
+ max="12"
91
+ value={count}
92
+ onchange={(e) => {
93
+ count = Math.max(2, Math.min(12, Number(e.currentTarget.value) || 4));
94
+ }}
95
+ class="w-11 rounded bg-slate-800 px-1 py-0.5 text-right font-mono text-slate-200"
96
+ />
97
+ </label>
98
+ </div>
99
+
100
+ {#if eligibleStrokes.length > 0}
101
+ <div class="flex max-h-20 flex-wrap gap-1 overflow-y-auto" role="listbox" aria-label="Parent strokes">
102
+ {#each eligibleStrokes as s, i (s.id)}
103
+ {@const on = effectiveStrokeIds.includes(s.id)}
104
+ <button
105
+ type="button"
106
+ role="option"
107
+ aria-selected={on}
108
+ title={`${i + 1}. ${s.brush.name} (${s.id})`}
109
+ class={`h-5 w-5 rounded ring-1 ${on ? 'ring-indigo-400' : 'ring-slate-700'}`}
110
+ style={`background: ${s.color}`}
111
+ onclick={() => toggleStroke(s.id)}
112
+ ></button>
113
+ {/each}
114
+ </div>
115
+ {/if}
116
+
117
+ {#each EVOLUTION_SLIDERS as c (c.path)}
118
+ {@const key = c.path as keyof EvolutionMutationRates}
119
+ <PaintSlider control={c} value={mutation[key]} onchange={(v) => (mutation = { ...mutation, [key]: v })} />
120
+ {/each}
121
+
122
+ <div class="flex flex-wrap gap-1">
123
+ <button
124
+ type="button"
125
+ class={button}
126
+ disabled={effectiveStrokeIds.length === 0 || editor.current.stroking}
127
+ onclick={() =>
128
+ run(() => {
129
+ controller.breedStrokes({ strokeIds: effectiveStrokeIds, count, mutation, seed });
130
+ seed += 1;
131
+ })}
132
+ >
133
+ Breed strokes
134
+ </button>
135
+ {#if activeProposalIds.length === 2}
136
+ <button
137
+ type="button"
138
+ class={button}
139
+ disabled={editor.current.stroking}
140
+ onclick={() =>
141
+ run(() => {
142
+ controller.breedProposal({ proposalIds: activeProposalIds, count, mutation, seed });
143
+ seed += 1;
144
+ })}
145
+ >
146
+ Cross over (2)
147
+ </button>
148
+ {/if}
149
+ {#if variants.length > 0}
150
+ <button
151
+ type="button"
152
+ class={button}
153
+ onclick={() =>
154
+ run(() => {
155
+ const sheet = exportProposalContactSheetPng(controller, variants);
156
+ contactSheetUrl = `data:image/png;base64,${sheet.base64}`;
157
+ })}
158
+ >
159
+ Contact sheet
160
+ </button>
161
+ {/if}
162
+ </div>
163
+
164
+ {#if variants.length > 0}
165
+ <div class="grid grid-cols-2 gap-1.5" data-testid="evolution-grid">
166
+ {#each variants as p (p.id)}
167
+ {@const g = paintProposalGhost(p)}
168
+ {@const picked = activeProposalIds.includes(p.id)}
169
+ <div
170
+ data-testid={`evolution-variant-${p.id}`}
171
+ class={`flex flex-col gap-1 rounded border p-1.5 ${picked ? 'border-indigo-500 bg-slate-800/90' : 'border-slate-800 bg-slate-950/60'}`}
172
+ >
173
+ <div class="flex items-center justify-between gap-1">
174
+ <button type="button" aria-pressed={picked} class={chip(picked)} onclick={() => toggleProposal(p.id)}>{p.title}</button>
175
+ </div>
176
+ {#if g}
177
+ <div class="flex h-16 items-center justify-center overflow-hidden rounded bg-slate-900/80 p-1">
178
+ <canvas use:drawPixels={g.data} width={g.width} height={g.height} role="img" aria-label={p.title} class="max-h-full max-w-full object-contain"></canvas>
179
+ </div>
180
+ {/if}
181
+ {#if p.rationale}
182
+ <span class="line-clamp-2 text-[10px] text-slate-400" title={p.rationale}>{p.rationale}</span>
183
+ {/if}
184
+ <div class="flex flex-wrap gap-1">
185
+ <button
186
+ type="button"
187
+ class={button}
188
+ disabled={editor.current.stroking}
189
+ onclick={() =>
190
+ run(() => {
191
+ controller.breedProposal({ proposalIds: [p.id], count, mutation, seed });
192
+ seed += 1;
193
+ })}
194
+ >
195
+ Breed from
196
+ </button>
197
+ <button type="button" class={button} onclick={() => run(() => store.accept(p.id))}>Accept</button>
198
+ <button type="button" class={button} onclick={() => run(() => store.reject(p.id))}>Dismiss</button>
199
+ </div>
200
+ </div>
201
+ {/each}
202
+ </div>
203
+ {/if}
204
+
205
+ {#if contactSheetUrl}
206
+ <img src={contactSheetUrl} alt="Variant contact sheet" class="w-full rounded border border-slate-800" data-testid="evolution-contact-sheet" />
207
+ {/if}
208
+
209
+ {#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
210
+ </div>
@@ -0,0 +1,10 @@
1
+ import { type PaintEditorController, type PaintProposals } from '@pixerate/editor/paint-editor';
2
+ interface Props {
3
+ controller: PaintEditorController;
4
+ /** Optional proposals store (defaults to `controller.proposals`). */
5
+ proposals?: PaintProposals;
6
+ class?: string;
7
+ }
8
+ declare const EvolutionPanel: import("svelte").Component<Props, {}, "">;
9
+ type EvolutionPanel = ReturnType<typeof EvolutionPanel>;
10
+ export default EvolutionPanel;
@@ -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, {}, "">;
@@ -13,6 +13,7 @@
13
13
  import StickerPanel from './StickerPanel.svelte';
14
14
  import StrokePanel from './StrokePanel.svelte';
15
15
  import PathStrokePanel from './PathStrokePanel.svelte';
16
+ import EvolutionPanel from './EvolutionPanel.svelte';
16
17
  import PaintEffectsPanel from './PaintEffectsPanel.svelte';
17
18
  import { chip, paintEffectsProvider } from './paintEditorRune.svelte.js';
18
19
 
@@ -28,6 +29,8 @@
28
29
  proposals?: PaintProposals;
29
30
  /** Strokes an agent is painting live alongside the user's (see `createPaintAgentStrokes`). */
30
31
  agentStrokes?: PaintAgentStrokes;
32
+ /** Follow device orientation (gyroscope tilt on iPhone and iPad) for thin-film iridescence view tilt. */
33
+ deviceTilt?: boolean;
31
34
  class?: string;
32
35
  /**
33
36
  * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
@@ -40,7 +43,7 @@
40
43
  /** Bring the editor on screen (switch to its tab) before the agent acts. */
41
44
  onplaybackreveal?: () => void | Promise<void>;
42
45
  }
43
- 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();
44
47
 
45
48
  /** Controls a paint plan can point at, by `{ kind: 'control', id }`. */
46
49
  const PAINT_CONTROLS: Record<string, string> = {
@@ -122,7 +125,7 @@
122
125
  <BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
123
126
  <div class="flex min-w-0 flex-1 flex-col">
124
127
  <!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
125
- <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" />
126
129
  {#if error}<p role="alert" class="mt-1 text-[11px] text-rose-400">{error}</p>{/if}
127
130
  </div>
128
131
  <div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
@@ -135,7 +138,7 @@
135
138
  {#if tab === 'color'}<ColorPanel {controller} />{/if}
136
139
  {#if tab === 'wet'}<WetPanel {controller} />{/if}
137
140
  {#if tab === 'paper'}<PaperPanel {controller} />{/if}
138
- {#if tab === 'layers'}<PaintLayersPanel {controller} /><StrokePanel {controller} /><PathStrokePanel {controller} /><StickerPanel {controller} />{/if}
141
+ {#if tab === 'layers'}<PaintLayersPanel {controller} /><StrokePanel {controller} /><PathStrokePanel {controller} /><EvolutionPanel {controller} {proposals} /><StickerPanel {controller} />{/if}
139
142
  {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
140
143
  </div>
141
144
  </div>
@@ -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
- <button type="button" class={`self-start ${button}`} onclick={() => run(() => controller.flattenRelief())}>Flatten relief</button>
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 &amp; 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>
@@ -13,6 +13,7 @@ import type StickerPanelComponent from './StickerPanel.svelte';
13
13
  import type StickerPickerComponent from './StickerPicker.svelte';
14
14
  import type StrokePanelComponent from './StrokePanel.svelte';
15
15
  import type PathStrokePanelComponent from './PathStrokePanel.svelte';
16
+ import type EvolutionPanelComponent from './EvolutionPanel.svelte';
16
17
  import type PaintEffectsPanelComponent from './PaintEffectsPanel.svelte';
17
18
  import type PaintGradientEditorComponent from './PaintGradientEditor.svelte';
18
19
  export { default as PaintEditor } from './PaintEditor.svelte';
@@ -29,12 +30,13 @@ export { default as StickerPanel } from './StickerPanel.svelte';
29
30
  export { default as StickerPicker } from './StickerPicker.svelte';
30
31
  export { default as StrokePanel } from './StrokePanel.svelte';
31
32
  export { default as PathStrokePanel } from './PathStrokePanel.svelte';
33
+ export { default as EvolutionPanel } from './EvolutionPanel.svelte';
32
34
  export { default as PaintEffectsPanel } from './PaintEffectsPanel.svelte';
33
35
  export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
34
36
  export { default as PaintSlider } from './PaintSlider.svelte';
35
37
  export { default as PaintPresenceLayer } from './PaintPresenceLayer.svelte';
36
38
  export { default as PaintProposalGhosts } from './PaintProposalGhosts.svelte';
37
- 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';
38
40
  export { createPaintDirector, createReactivePaintEditor, paintEditorState, paintEffectsProvider, usePaintEffectsTools } from './paintEditorRune.svelte.js';
39
41
  export type PaintEditorProps = ComponentProps<typeof PaintEditorComponent>;
40
42
  export type PaintCanvasProps = ComponentProps<typeof PaintCanvasComponent>;
@@ -50,5 +52,6 @@ export type StickerPanelProps = ComponentProps<typeof StickerPanelComponent>;
50
52
  export type StickerPickerProps = ComponentProps<typeof StickerPickerComponent>;
51
53
  export type StrokePanelProps = ComponentProps<typeof StrokePanelComponent>;
52
54
  export type PathStrokePanelProps = ComponentProps<typeof PathStrokePanelComponent>;
55
+ export type EvolutionPanelProps = ComponentProps<typeof EvolutionPanelComponent>;
53
56
  export type PaintEffectsPanelProps = ComponentProps<typeof PaintEffectsPanelComponent>;
54
57
  export type PaintGradientEditorProps = ComponentProps<typeof PaintGradientEditorComponent>;
@@ -14,10 +14,11 @@ export { default as StickerPanel } from './StickerPanel.svelte';
14
14
  export { default as StickerPicker } from './StickerPicker.svelte';
15
15
  export { default as StrokePanel } from './StrokePanel.svelte';
16
16
  export { default as PathStrokePanel } from './PathStrokePanel.svelte';
17
+ export { default as EvolutionPanel } from './EvolutionPanel.svelte';
17
18
  export { default as PaintEffectsPanel } from './PaintEffectsPanel.svelte';
18
19
  export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
19
20
  export { default as PaintSlider } from './PaintSlider.svelte';
20
21
  export { default as PaintPresenceLayer } from './PaintPresenceLayer.svelte';
21
22
  export { default as PaintProposalGhosts } from './PaintProposalGhosts.svelte';
22
- export { createPaintChangeWatcher, createPaintProposalState } from './paintProposalRune.svelte.js';
23
+ export { createMutationRequests, createPaintChangeWatcher, createPaintProposalState, } from './paintProposalRune.svelte.js';
23
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
- return { presence, director: new PaintDirector(controller, presence, options) };
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.203.0",
3
+ "version": "0.205.0",
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.273.0"
187
+ "@pixerate/editor": "0.275.0"
188
188
  },
189
189
  "peerDependencies": {
190
190
  "@dagrejs/dagre": "^1.1.4",