@pixerate/editor-svelte 0.165.12 → 0.167.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.
@@ -23,7 +23,9 @@
23
23
  return untrack(() => c.subscribe(() => tick++));
24
24
  });
25
25
  /** The open effect browser: filters to apply, or effects to change an effect layer to. */
26
- let browse = $state<null | 'filter' | 'change'>(null);
26
+ let browse = $state<null | 'filter' | 'change' | 'layer'>(null);
27
+ /** Clip the next effect layer to the layer below it. */
28
+ let clip = $state(false);
27
29
  let message = $state<string | null>(null);
28
30
  let open = $state<string | null>(null);
29
31
  const snapshot = $derived.by(() => {
@@ -84,9 +86,10 @@
84
86
  Provider effects for the active layer. An effect layer first: its effect (change it or
85
87
  rasterize it), a still frame or an animation for an effect that moves, and its controls. Then
86
88
  the provider filters applied to the layer (smart filters; applying one converts a pixel layer to
87
- a smart object in the same undo step). Layer styles (drop shadow, stroke…) are not here: they
88
- are part of the layer's properties (LayerEffectsPanel). New effect layers come from the layers
89
- panel. Without a provider it says why nothing can be added.
89
+ a smart object in the same undo step), each keeping the layer's transparency unless "Keep
90
+ transparency" is cleared. Then adding an effect layer, above the active layer and optionally
91
+ clipped to it. Layer styles (drop shadow, stroke…) are not here: they are part of the layer's
92
+ properties (LayerEffectsPanel). Without a provider it says why nothing can be added.
90
93
  -->
91
94
  <section class="flex flex-col gap-2 text-xs {className}" aria-label="Effects" data-image-provider-effects="">
92
95
  {#if !provider}<p class="text-[11px] text-muted-foreground">No effects provider is registered. Effects in the document are kept.</p>{/if}
@@ -162,7 +165,7 @@
162
165
  </div>
163
166
  {#if browse === 'filter'}
164
167
  {@const id = layer.id}
165
- <EffectsBrowser {provider} uses={['filter']} title="Filters" onpick={(e) => ((browse = null), run(() => controller.applyEffectFilter(id, e.id, { useSelection: true })))} />
168
+ <EffectsBrowser {provider} uses={['filter']} title="Filters" onpick={(e) => ((browse = null), run(() => controller.applyEffectFilter(id, e.id, { useSelection: true, preserveTransparency: true })))} />
166
169
  {/if}
167
170
  {/if}
168
171
  {#if layer && all.some((f) => f.kind === 'effect')}
@@ -181,6 +184,12 @@
181
184
  <button type="button" class={imageOutlineButtonClass} disabled={index === 0} aria-label="Move down" onclick={() => run(() => controller.updateEffectFilter(id, f.id, { index: index - 1 }))}>↓</button>
182
185
  <button type="button" class={imageOutlineButtonClass} aria-label="Remove {label}" onclick={() => run(() => controller.removeSmartFilter(id, f.id))}>✕</button>
183
186
  </div>
187
+ <label class="mt-0.5 flex items-center gap-1 pl-0.5 text-[11px] text-muted-foreground" title="Draw the filter only over the layer's own pixels; clear it to let the effect fill transparent areas">
188
+ <ImageCheckbox checked={f.preserveTransparency === true} label="{label}: keep transparency" onchange={(preserveTransparency) => {
189
+ run(() => controller.updateEffectFilter(id, f.id, { preserveTransparency }));
190
+ }} />
191
+ Keep transparency
192
+ </label>
184
193
  {#if open === f.id}
185
194
  <div class="mt-1 space-y-2">
186
195
  <label class="flex items-center gap-1">
@@ -206,7 +215,22 @@
206
215
  </ol>
207
216
  {/if}
208
217
  {#if layer && canFilter && !generator && !all.some((f) => f.kind === 'effect')}
209
- <p class="text-[11px] text-muted-foreground">No filters yet. A filter changes the layer's pixels and stays editable. Layer styles such as a drop shadow are in the layer's properties; new effect layers are added from the layers panel.</p>
218
+ <p class="text-[11px] text-muted-foreground">No filters yet. A filter changes the layer's pixels and stays editable. Layer styles such as a drop shadow are in the layer's properties.</p>
219
+ {/if}
220
+ {#if provider}
221
+ <div class="space-y-1" data-effect-part="add-layer">
222
+ <div class="flex items-center gap-1">
223
+ <span class="flex-1 text-[11px] font-semibold uppercase tracking-wider text-foreground">Effect layers</span>
224
+ <button type="button" class={imageOutlineButtonClass} aria-pressed={browse === 'layer'} onclick={() => (browse = browse === 'layer' ? null : 'layer')}>Add effect layer…</button>
225
+ </div>
226
+ <label class="flex items-center gap-1 text-[11px] text-muted-foreground" title="Show the new effect layer only over the pixels of the layer below it (a clipping mask)">
227
+ <ImageCheckbox checked={clip} label="Clip to layer below" onchange={(v) => (clip = v)} />
228
+ Clip to layer below
229
+ </label>
230
+ {#if browse === 'layer'}
231
+ <EffectsBrowser {provider} uses={EFFECT_LAYER_USES} title="Add an effect layer" onpick={(e) => ((browse = null), run(() => controller.addEffectLayer(e.id, { clipped: clip })))} />
232
+ {/if}
233
+ </div>
210
234
  {/if}
211
235
  {#if !layer}<p class="text-[11px] text-muted-foreground">Select a layer to apply effects to it.</p>{/if}
212
236
  {#if message}<p role="alert" class="text-[11px] text-destructive">{message}</p>{/if}
@@ -52,6 +52,12 @@
52
52
  return untrack(() => ({ width: MIXING_PAD_WIDTH, height: MIXING_PAD_HEIGHT, data: controller.mixingPad.pixels() }));
53
53
  });
54
54
  const R = $derived(wheelSize / 2);
55
+ /** Effect paint shows as a thumbnail of the effect in the paint well. */
56
+ const effectWell = $derived.by(() => {
57
+ const fx = snap.effectPaint;
58
+ const rendering = snap.effectPaintRendering;
59
+ return fx && !rendering ? untrack(() => controller.effectPaintThumbnail(64)) : null;
60
+ });
55
61
  const setHsv = (s: number, v: number) => controller.setColor(rgbToHexColor(...hsvToRgb(hsv.h, s, v)));
56
62
  const padPoint = (e: PointerEvent) => {
57
63
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
@@ -106,9 +112,10 @@
106
112
  <div class="flex items-center gap-2" data-testid="paint-source">
107
113
  <span
108
114
  class="h-5 flex-1 rounded ring-1 ring-slate-700"
109
- aria-label={snap.paint.type === 'gradient' ? 'Gradient paint' : 'Paint colour'}
115
+ aria-label={snap.effectPaint ? `Effect paint: ${snap.effectPaint.effect}` : snap.paint.type === 'gradient' ? 'Gradient paint' : 'Paint colour'}
110
116
  data-testid="paint-well"
111
- style:background={paintSourceCss(snap.paint, { paint: color, secondary: snap.secondaryColor })}
117
+ data-effect-paint={snap.effectPaint ? snap.effectPaint.effect : undefined}
118
+ style:background={effectWell ? `center / cover no-repeat url("${effectWell}")` : paintSourceCss(snap.paint, { paint: color, secondary: snap.secondaryColor })}
112
119
  ></span>
113
120
  <button
114
121
  type="button"
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
2
3
  import { PAPER_EFFECT_BLENDS, type PaintEditorController, type PaperEffectBlend } from '@pixerate/editor/paint-editor';
3
4
  import type { EffectUse } from '@pixerate/editor/effects';
4
5
  import EffectsBrowser from '../effects/EffectsBrowser.svelte';
@@ -21,14 +22,22 @@
21
22
  paper: controller.getPaperEffect(),
22
23
  layerEffect: activeId ? controller.getEffectLayer(activeId) : undefined,
23
24
  pixel: activeId ? controller.getLayer(activeId)?.type === 'pixel' : false,
25
+ effectPaint: editor.current.effectPaint,
26
+ rendering: editor.current.effectPaintRendering,
27
+ color: editor.current.color,
24
28
  };
25
29
  });
26
- type Browse = null | 'paper' | 'layer' | 'fill' | 'light';
30
+ const thumbnail = $derived.by(() => {
31
+ const { effectPaint, rendering } = view;
32
+ return effectPaint && !rendering ? untrack(() => controller.effectPaintThumbnail(48)) : null;
33
+ });
34
+ type Browse = null | 'paper' | 'layer' | 'fill' | 'light' | 'paint';
27
35
  const BROWSERS: Record<Exclude<Browse, null>, { uses: EffectUse[]; title: string }> = {
28
36
  paper: { uses: ['generator'], title: 'Effect paper' },
29
37
  layer: { uses: ['generator', 'overlay', 'title'], title: 'Effect layer' },
30
38
  fill: { uses: ['generator', 'overlay', 'title'], title: 'Fill the layer' },
31
39
  light: { uses: ['filter'], title: 'Light on the paint' },
40
+ paint: { uses: ['generator', 'overlay', 'title'], title: 'Paint with an effect' },
32
41
  };
33
42
  let browse = $state<Browse>(null);
34
43
  let error = $state<string | null>(null);
@@ -57,13 +66,15 @@
57
66
  else if (kind === 'layer') run(() => controller.addEffectLayer(id));
58
67
  else if (kind === 'fill') run(() => controller.fillLayerWithEffect(id));
59
68
  else if (kind === 'light' && layerId) run(() => controller.applyLightEffect(layerId, id));
69
+ else if (kind === 'paint') run(() => controller.setEffectPaint({ effect: id }));
60
70
  }
61
71
  </script>
62
72
 
63
73
  <!--
64
74
  Provider effects in the paint editor: effect paper (a generator over the sheet, multiplied so the
65
- grain shows), effect layers, effect fills and light baked onto the active layer's dried paint;
66
- plus the controls of the effect paper and of an active effect layer. Each change is one undo
75
+ grain shows), effect layers, effect fills, light baked onto the active layer's dried paint and
76
+ effect paint (brushes take their colours from the effect); plus the controls of the effect
77
+ paper, of an active effect layer and of the effect paint. Each change is one undo
67
78
  step (a slider drag is one). Without a provider it says why nothing can be added.
68
79
  -->
69
80
  <div class={`${panel} ${className}`} data-testid="paint-effects-panel" aria-busy={busy}>
@@ -73,6 +84,7 @@
73
84
  <button type="button" class={button} disabled={!provider} aria-pressed={browse === 'layer'} onclick={() => toggle('layer')}>Layer…</button>
74
85
  <button type="button" class={button} disabled={!provider || !view.pixel} aria-pressed={browse === 'fill'} onclick={() => toggle('fill')}>Fill…</button>
75
86
  <button type="button" class={button} disabled={!provider || !view.pixel} aria-pressed={browse === 'light'} onclick={() => toggle('light')} title="Bake a light, glow or grade into the active layer's dried paint">Light…</button>
87
+ <button type="button" class={button} disabled={!provider} aria-pressed={browse === 'paint'} onclick={() => toggle('paint')} title="Paint with an effect: brushes take each pixel's colour from it">Paint…</button>
76
88
  </div>
77
89
  {#if !provider}<p class="text-[11px] text-slate-400">No effects provider is registered. Effects in the painting are kept.</p>{/if}
78
90
  {#if browse}
@@ -105,6 +117,23 @@
105
117
  />
106
118
  </div>
107
119
  {/if}
120
+ {#if view.effectPaint}
121
+ {@const fx = view.effectPaint}
122
+ <div class="flex flex-col gap-1 rounded border border-slate-700 p-1" data-effect-part="paint">
123
+ <div class="flex items-center gap-1">
124
+ {#if thumbnail}<img src={thumbnail} alt="" class="h-5 w-8 rounded object-cover ring-1 ring-slate-700" data-testid="effect-paint-thumbnail" />{/if}
125
+ <span class="flex-1 truncate">Paint: {fx.effect}{view.rendering ? ' (rendering…)' : ''}</span>
126
+ <button type="button" class={button} aria-label="Stop painting with the effect" onclick={() => run(() => controller.setColor(view.color))}>✕</button>
127
+ </div>
128
+ <ImageEffectControls
129
+ {provider}
130
+ effect={fx.effect}
131
+ values={fx.values}
132
+ onchange={(k, v) => run(() => controller.setEffectPaint({ effect: fx.effect, values: { ...fx.values, [k]: v as never }, time: fx.time }))}
133
+ onpreset={(values) => run(() => controller.setEffectPaint({ effect: fx.effect, values: { ...fx.values, ...values } as never, time: fx.time }))}
134
+ />
135
+ </div>
136
+ {/if}
108
137
  {#if view.layerEffect && view.activeId}
109
138
  {@const fx = view.layerEffect}
110
139
  {@const layerId = view.activeId}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.165.12",
3
+ "version": "0.167.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.225.0"
168
+ "@pixerate/editor": "0.227.0"
169
169
  },
170
170
  "peerDependencies": {
171
171
  "@dagrejs/dagre": "^1.1.4",