@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.
|
|
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.
|
|
187
|
+
"@pixerate/editor": "0.274.0"
|
|
188
188
|
},
|
|
189
189
|
"peerDependencies": {
|
|
190
190
|
"@dagrejs/dagre": "^1.1.4",
|