@pixerate/editor-svelte 0.176.1 → 0.177.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.
@@ -40,6 +40,7 @@
40
40
  let water = $state<HTMLCanvasElement | null>(null);
41
41
  let size = $state({ width: 800, height: 600 });
42
42
  let predicted = $state<Array<{ x: number; y: number }>>([]);
43
+ let strokeDrag = $state<{ id: string; dx: number; dy: number } | null>(null);
43
44
  const presenter = $derived(new PaintCanvasPresenter(controller));
44
45
  const gesture = $derived(
45
46
  new PaintGesture(controller, () => {
@@ -117,7 +118,7 @@
117
118
  const transform = $derived(viewCssTransform(snap.view, doc, size));
118
119
  const lines = $derived.by(() => {
119
120
  void snap;
120
- return showGuides ? guideOverlay(controller) : [];
121
+ return showGuides ? guideOverlay(controller, strokeDrag) : [];
121
122
  });
122
123
  </script>
123
124
 
@@ -130,7 +131,7 @@
130
131
  aria-label="Paint canvas"
131
132
  data-testid="paint-canvas"
132
133
  class={`relative overflow-hidden bg-slate-950 outline-none touch-none select-none ${className}`}
133
- style={`cursor: ${tool === 'pan' ? 'grab' : tool === 'eyedropper' ? 'copy' : 'crosshair'}`}
134
+ style={`cursor: ${tool === 'pan' ? 'grab' : tool === 'eyedropper' ? 'copy' : tool === 'stroke' ? 'default' : 'crosshair'}`}
134
135
  onpointerdown={(e) => {
135
136
  viewport?.focus();
136
137
  if (gesture.down(e)) (e.target as Element).setPointerCapture?.(e.pointerId);
@@ -139,14 +140,18 @@
139
140
  onpointermove={(e) => {
140
141
  gesture.move(e);
141
142
  if (gesture.predicted.length || predicted.length) predicted = gesture.predicted.map((p) => ({ x: p.x, y: p.y }));
143
+ if (gesture.strokeDrag || strokeDrag) strokeDrag = gesture.strokeDrag;
142
144
  }}
143
145
  onpointerup={(e) => {
144
146
  gesture.up(e);
145
147
  predicted = [];
148
+ strokeDrag = null;
149
+ onerror?.(gesture.error);
146
150
  }}
147
151
  onpointercancel={() => {
148
152
  gesture.cancel();
149
153
  predicted = [];
154
+ strokeDrag = null;
150
155
  }}
151
156
  onkeydown={(e) => {
152
157
  if (handlePaintShortcut(controller, gesture, e)) e.preventDefault();
@@ -176,8 +181,8 @@
176
181
  y1={l.from.y}
177
182
  x2={l.to.x}
178
183
  y2={l.to.y}
179
- stroke={l.kind === 'symmetry' ? '#22d3ee' : l.kind === 'stencil' ? '#fbbf24' : l.kind === 'sticker' ? '#818cf8' : '#f472b6'}
180
- stroke-opacity={l.kind === 'symmetry' || l.kind === 'stencil' || l.kind === 'sticker' ? 0.8 : 0.35}
184
+ stroke={l.kind === 'symmetry' ? '#22d3ee' : l.kind === 'stencil' ? '#fbbf24' : l.kind === 'stroke' || l.kind === 'sticker' ? '#818cf8' : '#f472b6'}
185
+ stroke-opacity={l.kind === 'symmetry' || l.kind === 'stencil' || l.kind === 'stroke' || l.kind === 'sticker' ? 0.8 : 0.35}
181
186
  stroke-width={1 / snap.view.zoom}
182
187
  stroke-dasharray={l.kind === 'symmetry' ? `${6 / snap.view.zoom} ${4 / snap.view.zoom}` : undefined}
183
188
  />
@@ -11,6 +11,7 @@
11
11
  import PaperPanel from './PaperPanel.svelte';
12
12
  import WetPanel from './WetPanel.svelte';
13
13
  import StickerPanel from './StickerPanel.svelte';
14
+ import StrokePanel from './StrokePanel.svelte';
14
15
  import PaintEffectsPanel from './PaintEffectsPanel.svelte';
15
16
  import { chip, paintEffectsProvider } from './paintEditorRune.svelte.js';
16
17
 
@@ -128,7 +129,7 @@
128
129
  {#if tab === 'color'}<ColorPanel {controller} />{/if}
129
130
  {#if tab === 'wet'}<WetPanel {controller} />{/if}
130
131
  {#if tab === 'paper'}<PaperPanel {controller} />{/if}
131
- {#if tab === 'layers'}<PaintLayersPanel {controller} /><StickerPanel {controller} />{/if}
132
+ {#if tab === 'layers'}<PaintLayersPanel {controller} /><StrokePanel {controller} /><StickerPanel {controller} />{/if}
132
133
  {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
133
134
  </div>
134
135
  </div>
@@ -31,6 +31,7 @@
31
31
  <div class="flex flex-wrap gap-1">
32
32
  <button type="button" class={button} onclick={() => run(() => controller.addLayer())}>New layer</button>
33
33
  <button type="button" class={button} onclick={() => run(() => controller.addLayer({ type: 'group' }))}>New group</button>
34
+ <button type="button" class={button} title="A layer whose strokes stay editable" onclick={() => run(() => controller.addLayer({ type: 'strokes' }))}>New stroke layer</button>
34
35
  <button type="button" class={button} disabled={!active} onclick={() => active && run(() => controller.duplicateLayer(active.id))}>Duplicate</button>
35
36
  <button type="button" class={button} disabled={!active} aria-label="Move layer up" onclick={() => move(1)}>↑</button>
36
37
  <button type="button" class={button} disabled={!active} aria-label="Move layer down" onclick={() => move(-1)}>↓</button>
@@ -75,7 +76,7 @@
75
76
  }}
76
77
  />
77
78
  {:else}
78
- <span class="flex-1 truncate" role="presentation" ondblclick={() => (renaming = l.id)}>{l.type === 'group' ? '▸ ' : ''}{l.name}</span>
79
+ <span class="flex-1 truncate" role="presentation" ondblclick={() => (renaming = l.id)}>{l.type === 'group' ? '▸ ' : ''}{#if l.liveStrokes !== undefined}<span aria-label="Stroke layer" title={`Stroke layer: ${l.liveStrokes} editable strokes`}>✎ </span>{/if}{l.name}</span>
79
80
  {/if}
80
81
  {#if l.locks.pixels || l.locks.all}<span title="Locked">🔒</span>{/if}
81
82
  </li>
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import { STROKE_SLIDERS, liveStrokeRefusal, readPath, type PaintEditorController } from '@pixerate/editor/paint-editor';
3
+ import PaintSlider from './PaintSlider.svelte';
4
+ import { attempt, button, paintEditorState, panel } from './paintEditorRune.svelte.js';
5
+
6
+ interface Props {
7
+ controller: PaintEditorController;
8
+ class?: string;
9
+ }
10
+ let { controller, class: className = '' }: Props = $props();
11
+ const editor = paintEditorState(() => controller);
12
+ const layerId = $derived(editor.current.document.activeLayerId);
13
+ const strokes = $derived(layerId ? editor.current.document.liveStrokes?.[layerId] : undefined);
14
+ const index = $derived(strokes ? strokes.findIndex((s) => s.id === editor.current.selectedStroke) : -1);
15
+ const sel = $derived(strokes && index >= 0 ? strokes[index] : null);
16
+ const brushes = $derived(editor.current.brushes.filter((b) => !liveStrokeRefusal(b)));
17
+ let error = $state<string | null>(null);
18
+ const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
19
+ </script>
20
+
21
+ <!-- The active stroke layer's strokes: pick one (here, or with the Select stroke tool on the canvas) to change its brush, size, opacity, flow, colour or seed, reorder or delete it. The layer redraws exactly; each change is one undo step. -->
22
+ {#if layerId && strokes}
23
+ <div class={`${panel} ${className}`} data-testid="stroke-panel">
24
+ <div class="flex items-center justify-between">
25
+ <span class="font-medium text-slate-300">Strokes ({strokes.length})</span>
26
+ <button type="button" class={button} onclick={() => run(() => controller.rasterizeLiveStrokes(layerId))}>Rasterize</button>
27
+ </div>
28
+ <div class="flex max-h-24 flex-wrap gap-1 overflow-y-auto" role="listbox" aria-label="Strokes">
29
+ {#each strokes as s, i (s.id)}
30
+ <button type="button" role="option" aria-selected={s.id === editor.current.selectedStroke} title={`${i + 1}. ${s.brush.name} (${s.id})`} class={`h-5 w-5 rounded ring-1 ${s.id === editor.current.selectedStroke ? 'ring-indigo-400' : 'ring-slate-700'}`} style={`background: ${s.color}`} onclick={() => controller.selectStroke(s.id)}></button>
31
+ {/each}
32
+ </div>
33
+ {#if sel}
34
+ <div class="flex flex-col gap-1.5" data-testid="stroke-edit">
35
+ <label class="flex items-center justify-between gap-2">
36
+ <span class="text-slate-400">Brush</span>
37
+ <select aria-label="Stroke brush" value={brushes.some((b) => b.id === sel.brush.id) ? sel.brush.id : ''} onchange={(e) => { const brushId = e.currentTarget.value; if (brushId) run(() => controller.updateStroke(sel.id, { brushId })); }} class="max-w-[10rem] rounded bg-slate-800 px-1 py-0.5">
38
+ {#if !brushes.some((b) => b.id === sel.brush.id)}<option value="">{sel.brush.name}</option>{/if}
39
+ {#each brushes as b (b.id)}<option value={b.id}>{b.name}</option>{/each}
40
+ </select>
41
+ </label>
42
+ {#each STROKE_SLIDERS as c (c.path)}
43
+ <PaintSlider control={c} value={readPath(sel.brush, c.path)} oncommit={(v) => run(() => controller.updateStroke(sel.id, { brush: { tip: { [c.path.split('.')[1]]: v } } }))} />
44
+ {/each}
45
+ <label class="flex items-center justify-between">
46
+ <span class="text-slate-400">Colour</span>
47
+ <input type="color" aria-label="Stroke colour" value={sel.color} onchange={(e) => { const color = e.currentTarget.value; run(() => controller.updateStroke(sel.id, { color })); }} class="h-6 w-10 rounded bg-transparent" />
48
+ </label>
49
+ <div class="flex flex-wrap gap-1">
50
+ <button type="button" class={button} onclick={() => run(() => controller.updateStroke(sel.id, { seed: (sel.seed + 1) % 2147483647 }))}>New seed</button>
51
+ <button type="button" class={button} disabled={index === strokes.length - 1} aria-label="Move stroke up" onclick={() => run(() => controller.reorderStroke(sel.id, index + 1))}>↑</button>
52
+ <button type="button" class={button} disabled={index === 0} aria-label="Move stroke down" onclick={() => run(() => controller.reorderStroke(sel.id, index - 1))}>↓</button>
53
+ <button type="button" class={button} onclick={() => run(() => { controller.removeStrokes([sel.id]); controller.selectStroke(null); })}>Delete</button>
54
+ </div>
55
+ </div>
56
+ {/if}
57
+ {#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
58
+ </div>
59
+ {/if}
@@ -0,0 +1,8 @@
1
+ import { type PaintEditorController } from '@pixerate/editor/paint-editor';
2
+ interface Props {
3
+ controller: PaintEditorController;
4
+ class?: string;
5
+ }
6
+ declare const StrokePanel: import("svelte").Component<Props, {}, "">;
7
+ type StrokePanel = ReturnType<typeof StrokePanel>;
8
+ export default StrokePanel;
@@ -8,6 +8,7 @@ import type ColorPanelComponent from './ColorPanel.svelte';
8
8
  import type PaintLayersPanelComponent from './PaintLayersPanel.svelte';
9
9
  import type WetPanelComponent from './WetPanel.svelte';
10
10
  import type StickerPanelComponent from './StickerPanel.svelte';
11
+ import type StrokePanelComponent from './StrokePanel.svelte';
11
12
  import type PaintEffectsPanelComponent from './PaintEffectsPanel.svelte';
12
13
  import type PaintGradientEditorComponent from './PaintGradientEditor.svelte';
13
14
  export { default as PaintEditor } from './PaintEditor.svelte';
@@ -19,6 +20,7 @@ export { default as ColorPanel } from './ColorPanel.svelte';
19
20
  export { default as PaintLayersPanel } from './PaintLayersPanel.svelte';
20
21
  export { default as WetPanel } from './WetPanel.svelte';
21
22
  export { default as StickerPanel } from './StickerPanel.svelte';
23
+ export { default as StrokePanel } from './StrokePanel.svelte';
22
24
  export { default as PaintEffectsPanel } from './PaintEffectsPanel.svelte';
23
25
  export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
24
26
  export { default as PaintSlider } from './PaintSlider.svelte';
@@ -33,5 +35,6 @@ export type ColorPanelProps = ComponentProps<typeof ColorPanelComponent>;
33
35
  export type PaintLayersPanelProps = ComponentProps<typeof PaintLayersPanelComponent>;
34
36
  export type WetPanelProps = ComponentProps<typeof WetPanelComponent>;
35
37
  export type StickerPanelProps = ComponentProps<typeof StickerPanelComponent>;
38
+ export type StrokePanelProps = ComponentProps<typeof StrokePanelComponent>;
36
39
  export type PaintEffectsPanelProps = ComponentProps<typeof PaintEffectsPanelComponent>;
37
40
  export type PaintGradientEditorProps = ComponentProps<typeof PaintGradientEditorComponent>;
@@ -7,6 +7,7 @@ export { default as ColorPanel } from './ColorPanel.svelte';
7
7
  export { default as PaintLayersPanel } from './PaintLayersPanel.svelte';
8
8
  export { default as WetPanel } from './WetPanel.svelte';
9
9
  export { default as StickerPanel } from './StickerPanel.svelte';
10
+ export { default as StrokePanel } from './StrokePanel.svelte';
10
11
  export { default as PaintEffectsPanel } from './PaintEffectsPanel.svelte';
11
12
  export { default as PaintGradientEditor } from './PaintGradientEditor.svelte';
12
13
  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.176.1",
3
+ "version": "0.177.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -165,7 +165,7 @@
165
165
  "**/*.css"
166
166
  ],
167
167
  "dependencies": {
168
- "@pixerate/editor": "0.239.0"
168
+ "@pixerate/editor": "0.240.0"
169
169
  },
170
170
  "peerDependencies": {
171
171
  "@dagrejs/dagre": "^1.1.4",