@pixerate/editor-svelte 0.203.0 → 0.204.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.
@@ -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;
@@ -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
 
@@ -135,7 +136,7 @@
135
136
  {#if tab === 'color'}<ColorPanel {controller} />{/if}
136
137
  {#if tab === 'wet'}<WetPanel {controller} />{/if}
137
138
  {#if tab === 'paper'}<PaperPanel {controller} />{/if}
138
- {#if tab === 'layers'}<PaintLayersPanel {controller} /><StrokePanel {controller} /><PathStrokePanel {controller} /><StickerPanel {controller} />{/if}
139
+ {#if tab === 'layers'}<PaintLayersPanel {controller} /><StrokePanel {controller} /><PathStrokePanel {controller} /><EvolutionPanel {controller} {proposals} /><StickerPanel {controller} />{/if}
139
140
  {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
140
141
  </div>
141
142
  </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,6 +30,7 @@ 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';
@@ -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,6 +14,7 @@ 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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.203.0",
3
+ "version": "0.204.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.274.0"
188
188
  },
189
189
  "peerDependencies": {
190
190
  "@dagrejs/dagre": "^1.1.4",