@pixerate/editor-svelte 0.124.1 → 0.125.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.
- package/dist/image-editor/AdjustmentLayersPanel.svelte +20 -40
- package/dist/image-editor/BrushOptionsBar.svelte +6 -35
- package/dist/image-editor/CanvasSizePanel.svelte +13 -22
- package/dist/image-editor/ControlSlider.svelte +2 -11
- package/dist/image-editor/LayerEffectsPanel.svelte +27 -35
- package/dist/image-editor/LayerPanel.svelte +14 -3
- package/dist/image-editor/SelectionOptionsBar.svelte +15 -22
- package/dist/image-editor/ShapeOptionsBar.svelte +6 -5
- package/dist/image-editor/controls/ImageCheckbox.svelte +19 -0
- package/dist/image-editor/controls/ImageCheckbox.svelte.d.ts +4 -0
- package/dist/image-editor/controls/ImageRange.svelte +26 -0
- package/dist/image-editor/controls/ImageRange.svelte.d.ts +4 -0
- package/dist/image-editor/controls/ImageSelect.svelte +29 -0
- package/dist/image-editor/controls/ImageSelect.svelte.d.ts +4 -0
- package/dist/image-editor/controls/controls.d.ts +60 -0
- package/dist/image-editor/controls/controls.js +13 -0
- package/dist/image-editor/index.d.ts +4 -0
- package/dist/image-editor/index.js +4 -0
- package/package.json +2 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
3
|
+
import ImageSelect from './controls/ImageSelect.svelte';
|
|
4
|
+
import { imageSectionTitleClass } from './styles.js';
|
|
3
5
|
import type {
|
|
4
6
|
AdjustmentKind,
|
|
5
7
|
AdjustmentLayer,
|
|
@@ -109,11 +111,13 @@
|
|
|
109
111
|
<span class={imageSectionTitleClass}>Adjustment Layers</span>
|
|
110
112
|
</div>
|
|
111
113
|
<div class="flex gap-1.5">
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
|
|
114
|
+
<ImageSelect
|
|
115
|
+
label="New adjustment layer kind"
|
|
116
|
+
class="flex-1"
|
|
117
|
+
value={newKind}
|
|
118
|
+
options={ADJUSTMENT_KINDS.map((k) => ({ value: k.kind, label: k.label }))}
|
|
119
|
+
onchange={(v) => (newKind = v as typeof newKind)}
|
|
120
|
+
/>
|
|
117
121
|
<button type="button" class="px-2 py-1 rounded bg-primary text-primary-foreground hover:bg-primary/90" onclick={addLayer}>
|
|
118
122
|
Add Layer
|
|
119
123
|
</button>
|
|
@@ -129,13 +133,7 @@
|
|
|
129
133
|
? 'bg-primary/20 ring-1 ring-primary/50'
|
|
130
134
|
: 'bg-muted/60'}"
|
|
131
135
|
>
|
|
132
|
-
<
|
|
133
|
-
type="checkbox"
|
|
134
|
-
aria-label="Show {label}"
|
|
135
|
-
checked={layer.enabled}
|
|
136
|
-
onchange={() => controller.toggleAdjustmentLayer(layer.id)}
|
|
137
|
-
class="accent-primary"
|
|
138
|
-
/>
|
|
136
|
+
<ImageCheckbox label="Show {label}" checked={layer.enabled} onchange={() => controller.toggleAdjustmentLayer(layer.id)} />
|
|
139
137
|
<button
|
|
140
138
|
type="button"
|
|
141
139
|
class="flex-1 text-left truncate {layer.enabled ? 'text-foreground' : 'text-muted-foreground line-through'}"
|
|
@@ -183,16 +181,13 @@
|
|
|
183
181
|
<div class="flex flex-col gap-2 rounded border border-border p-2" data-testid="adjustment-layer-editor">
|
|
184
182
|
<div class="flex items-center gap-2">
|
|
185
183
|
<span class="text-muted-foreground">Blend</span>
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
class="
|
|
184
|
+
<ImageSelect
|
|
185
|
+
label="Blend mode"
|
|
186
|
+
class="flex-1"
|
|
189
187
|
value={layer.blend}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<option value={m.id}>{m.label}</option>
|
|
194
|
-
{/each}
|
|
195
|
-
</select>
|
|
188
|
+
options={BLEND_MODES.map((m) => ({ value: m.id, label: m.label }))}
|
|
189
|
+
onchange={(v) => controller.updateAdjustmentLayer(layer.id, { blend: v as BlendMode })}
|
|
190
|
+
/>
|
|
196
191
|
</div>
|
|
197
192
|
<ControlSlider
|
|
198
193
|
spec={ADJUSTMENT_LAYER_OPACITY_CONTROL}
|
|
@@ -265,34 +260,19 @@
|
|
|
265
260
|
|
|
266
261
|
{#if layer.kind === 'hueSaturation' && range === -1}
|
|
267
262
|
<label class="flex items-center gap-2 text-foreground">
|
|
268
|
-
<
|
|
269
|
-
type="checkbox"
|
|
270
|
-
checked={layer.params.colorize}
|
|
271
|
-
onchange={(e) => setParam(layer, 'colorize', (e.target as HTMLInputElement).checked)}
|
|
272
|
-
class="accent-primary"
|
|
273
|
-
/>
|
|
263
|
+
<ImageCheckbox label="Colorize" checked={layer.params.colorize} onchange={(on) => setParam(layer, 'colorize', on)} />
|
|
274
264
|
Colorize
|
|
275
265
|
</label>
|
|
276
266
|
{/if}
|
|
277
267
|
{#if layer.kind === 'colorBalance' || layer.kind === 'photoFilter'}
|
|
278
268
|
<label class="flex items-center gap-2 text-foreground">
|
|
279
|
-
<
|
|
280
|
-
type="checkbox"
|
|
281
|
-
checked={layer.params.preserveLuminosity}
|
|
282
|
-
onchange={(e) => setParam(layer, 'preserveLuminosity', (e.target as HTMLInputElement).checked)}
|
|
283
|
-
class="accent-primary"
|
|
284
|
-
/>
|
|
269
|
+
<ImageCheckbox label="Preserve luminosity" checked={layer.params.preserveLuminosity} onchange={(on) => setParam(layer, 'preserveLuminosity', on)} />
|
|
285
270
|
Preserve luminosity
|
|
286
271
|
</label>
|
|
287
272
|
{/if}
|
|
288
273
|
{#if layer.kind === 'blackWhite'}
|
|
289
274
|
<label class="flex items-center gap-2 text-foreground">
|
|
290
|
-
<
|
|
291
|
-
type="checkbox"
|
|
292
|
-
checked={layer.params.tint !== null}
|
|
293
|
-
onchange={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).checked ? '#e1d3b3' : null)}
|
|
294
|
-
class="accent-primary"
|
|
295
|
-
/>
|
|
275
|
+
<ImageCheckbox label="Tint" checked={layer.params.tint !== null} onchange={(on) => setParam(layer, 'tint', on ? '#e1d3b3' : null)} />
|
|
296
276
|
Tint
|
|
297
277
|
{#if layer.params.tint}
|
|
298
278
|
<input
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
2
3
|
import { imageChipClass, imageColorInputClass } from './styles.js';
|
|
3
4
|
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
4
5
|
import {
|
|
@@ -136,23 +137,11 @@
|
|
|
136
137
|
{/each}
|
|
137
138
|
<div class="flex flex-wrap gap-3 text-foreground">
|
|
138
139
|
<label class="flex items-center gap-1.5">
|
|
139
|
-
<
|
|
140
|
-
type="checkbox"
|
|
141
|
-
aria-label="Load the brush after each stroke"
|
|
142
|
-
checked={view.mixer.loadEachStroke}
|
|
143
|
-
onchange={(e) => controller.setPaintOptions({ mixer: { loadEachStroke: (e.target as HTMLInputElement).checked } })}
|
|
144
|
-
class="accent-primary"
|
|
145
|
-
/>
|
|
140
|
+
<ImageCheckbox label="Load the brush after each stroke" checked={view.mixer.loadEachStroke} onchange={(on) => controller.setPaintOptions({ mixer: { loadEachStroke: on } })} />
|
|
146
141
|
Load each stroke
|
|
147
142
|
</label>
|
|
148
143
|
<label class="flex items-center gap-1.5">
|
|
149
|
-
<
|
|
150
|
-
type="checkbox"
|
|
151
|
-
aria-label="Clean the brush after each stroke"
|
|
152
|
-
checked={view.mixer.cleanEachStroke}
|
|
153
|
-
onchange={(e) => controller.setPaintOptions({ mixer: { cleanEachStroke: (e.target as HTMLInputElement).checked } })}
|
|
154
|
-
class="accent-primary"
|
|
155
|
-
/>
|
|
144
|
+
<ImageCheckbox label="Clean the brush after each stroke" checked={view.mixer.cleanEachStroke} onchange={(on) => controller.setPaintOptions({ mixer: { cleanEachStroke: on } })} />
|
|
156
145
|
Clean each stroke
|
|
157
146
|
</label>
|
|
158
147
|
</div>
|
|
@@ -200,13 +189,7 @@
|
|
|
200
189
|
<div class="flex flex-wrap gap-3 text-foreground">
|
|
201
190
|
{#each RETOUCH_TOGGLES.filter((t) => t.tools.includes(view.tool as never)) as t (t.id)}
|
|
202
191
|
<label class="flex items-center gap-1.5">
|
|
203
|
-
<
|
|
204
|
-
type="checkbox"
|
|
205
|
-
aria-label={t.label}
|
|
206
|
-
checked={view.retouch[t.id]}
|
|
207
|
-
onchange={(e) => controller.setPaintOptions({ retouch: { [t.id]: (e.target as HTMLInputElement).checked } })}
|
|
208
|
-
class="accent-primary"
|
|
209
|
-
/>
|
|
192
|
+
<ImageCheckbox label={t.label} checked={view.retouch[t.id]} onchange={(on) => controller.setPaintOptions({ retouch: { [t.id]: on } })} />
|
|
210
193
|
{t.label}
|
|
211
194
|
</label>
|
|
212
195
|
{/each}
|
|
@@ -216,23 +199,11 @@
|
|
|
216
199
|
|
|
217
200
|
<div class="flex flex-wrap gap-3 text-foreground" role="group" aria-label="Pressure">
|
|
218
201
|
<label class="flex items-center gap-1.5">
|
|
219
|
-
<
|
|
220
|
-
type="checkbox"
|
|
221
|
-
aria-label="Pressure controls size"
|
|
222
|
-
checked={view.brush.pressureSize}
|
|
223
|
-
onchange={(e) => controller.setPaintOptions({ brush: { pressureSize: (e.target as HTMLInputElement).checked } })}
|
|
224
|
-
class="accent-primary"
|
|
225
|
-
/>
|
|
202
|
+
<ImageCheckbox label="Pressure controls size" checked={view.brush.pressureSize} onchange={(on) => controller.setPaintOptions({ brush: { pressureSize: on } })} />
|
|
226
203
|
Pressure size
|
|
227
204
|
</label>
|
|
228
205
|
<label class="flex items-center gap-1.5">
|
|
229
|
-
<
|
|
230
|
-
type="checkbox"
|
|
231
|
-
aria-label="Pressure controls opacity"
|
|
232
|
-
checked={view.brush.pressureOpacity}
|
|
233
|
-
onchange={(e) => controller.setPaintOptions({ brush: { pressureOpacity: (e.target as HTMLInputElement).checked } })}
|
|
234
|
-
class="accent-primary"
|
|
235
|
-
/>
|
|
206
|
+
<ImageCheckbox label="Pressure controls opacity" checked={view.brush.pressureOpacity} onchange={(on) => controller.setPaintOptions({ brush: { pressureOpacity: on } })} />
|
|
236
207
|
Pressure opacity
|
|
237
208
|
</label>
|
|
238
209
|
</div>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageSelect from './controls/ImageSelect.svelte';
|
|
3
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
2
4
|
import type { ImageEditorController, TransformAnchor } from '@pixerate/editor/image-editor';
|
|
3
5
|
import { CANVAS_SIZE_PRESETS, isTransformAnchor } from '@pixerate/editor/image-editor';
|
|
4
6
|
import { untrack } from 'svelte';
|
|
5
|
-
import { imagePrimaryButtonClass
|
|
7
|
+
import { imagePrimaryButtonClass } from './styles.js';
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
controller: ImageEditorController;
|
|
@@ -56,10 +58,7 @@
|
|
|
56
58
|
height = val;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function onPresetSelect(
|
|
60
|
-
const target = e.currentTarget as HTMLSelectElement;
|
|
61
|
-
const presetId = target.value;
|
|
62
|
-
target.value = '';
|
|
61
|
+
function onPresetSelect(presetId: string) {
|
|
63
62
|
const preset = CANVAS_SIZE_PRESETS.find((p) => p.id === presetId);
|
|
64
63
|
if (!preset) return;
|
|
65
64
|
width = preset.width;
|
|
@@ -85,18 +84,15 @@
|
|
|
85
84
|
{/if}
|
|
86
85
|
|
|
87
86
|
<div class="flex flex-col gap-1.5">
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
<span class="text-muted-foreground text-[11px]">Preset</span>
|
|
88
|
+
<ImageSelect
|
|
89
|
+
class="w-full"
|
|
90
|
+
label="Preset canvas sizes"
|
|
91
|
+
value=""
|
|
92
|
+
placeholder="Choose preset…"
|
|
93
|
+
options={CANVAS_SIZE_PRESETS.map((preset) => ({ value: preset.id, label: `${preset.label} (${preset.width} × ${preset.height})` }))}
|
|
92
94
|
onchange={onPresetSelect}
|
|
93
|
-
|
|
94
|
-
>
|
|
95
|
-
<option value="">Choose preset…</option>
|
|
96
|
-
{#each CANVAS_SIZE_PRESETS as preset (preset.id)}
|
|
97
|
-
<option value={preset.id}>{preset.label} ({preset.width} × {preset.height})</option>
|
|
98
|
-
{/each}
|
|
99
|
-
</select>
|
|
95
|
+
/>
|
|
100
96
|
</div>
|
|
101
97
|
|
|
102
98
|
<div class="grid grid-cols-2 gap-2 items-center">
|
|
@@ -129,12 +125,7 @@
|
|
|
129
125
|
|
|
130
126
|
<div class="flex items-center gap-2">
|
|
131
127
|
<label class="flex items-center gap-1.5 cursor-pointer text-muted-foreground select-none">
|
|
132
|
-
<
|
|
133
|
-
type="checkbox"
|
|
134
|
-
class="rounded border-border text-primary focus:ring-primary"
|
|
135
|
-
checked={lockAspect}
|
|
136
|
-
onchange={(e) => (lockAspect = (e.currentTarget as HTMLInputElement).checked)}
|
|
137
|
-
/>
|
|
128
|
+
<ImageCheckbox label="Lock aspect ratio" checked={lockAspect} onchange={(on) => (lockAspect = on)} />
|
|
138
129
|
<span>Lock aspect ratio</span>
|
|
139
130
|
</label>
|
|
140
131
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageRange from './controls/ImageRange.svelte';
|
|
2
3
|
import type { ControlSpec } from '@pixerate/editor/image-editor';
|
|
3
4
|
import { controlTrackBackground, formatControlValue } from '@pixerate/editor/image-editor';
|
|
4
5
|
|
|
@@ -24,17 +25,7 @@
|
|
|
24
25
|
<span>{name}</span>
|
|
25
26
|
<span class="font-mono">{formatControlValue(spec, value)}</span>
|
|
26
27
|
</div>
|
|
27
|
-
<
|
|
28
|
-
type="range"
|
|
29
|
-
min={spec.min}
|
|
30
|
-
max={spec.max}
|
|
31
|
-
step={spec.step}
|
|
32
|
-
aria-label={name}
|
|
33
|
-
{disabled}
|
|
34
|
-
{value}
|
|
35
|
-
oninput={(e) => onchange(Number((e.target as HTMLInputElement).value))}
|
|
36
|
-
class="w-full accent-primary disabled:opacity-40"
|
|
37
|
-
/>
|
|
28
|
+
<ImageRange min={spec.min} max={spec.max} step={spec.step} label={name} {disabled} {value} oninput={onchange} />
|
|
38
29
|
{#if track}
|
|
39
30
|
<div class="h-0.5 rounded-full opacity-70" style="background: {track}"></div>
|
|
40
31
|
{/if}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
3
|
+
import ImageRange from './controls/ImageRange.svelte';
|
|
4
|
+
import ImageSelect from './controls/ImageSelect.svelte';
|
|
5
|
+
import { imageIconButtonClass } from './styles.js';
|
|
3
6
|
import type { EffectControl, ImageEditorController, LayerEffectKind } from '@pixerate/editor/image-editor';
|
|
4
7
|
import { LAYER_EFFECT_MENU, effectControlChange, layerEffectControls, layerEffectsPanel } from '@pixerate/editor/image-editor';
|
|
5
8
|
import { untrack } from 'svelte';
|
|
@@ -57,10 +60,8 @@
|
|
|
57
60
|
});
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function add(
|
|
61
|
-
const
|
|
62
|
-
const kind = select.value as LayerEffectKind;
|
|
63
|
-
select.value = '';
|
|
63
|
+
function add(value: string) {
|
|
64
|
+
const kind = value as LayerEffectKind;
|
|
64
65
|
const id = view.id;
|
|
65
66
|
if (!kind || !id) return;
|
|
66
67
|
attempt(() => (open = controller.addLayerEffect(id, kind).effect.id));
|
|
@@ -84,13 +85,11 @@
|
|
|
84
85
|
<div class="flex flex-wrap items-center gap-2">
|
|
85
86
|
<span class="font-semibold text-foreground">fx Effects</span>
|
|
86
87
|
<label class="flex items-center gap-1 text-foreground">
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
aria-label="Effects on"
|
|
88
|
+
<ImageCheckbox
|
|
89
|
+
label="Effects on"
|
|
90
90
|
checked={view.model.enabled}
|
|
91
91
|
disabled={view.model.readOnly}
|
|
92
|
-
onchange={(
|
|
93
|
-
class="accent-primary"
|
|
92
|
+
onchange={(on) => attempt(() => controller.setLayerEffectsEnabled(id, on))}
|
|
94
93
|
/>
|
|
95
94
|
On
|
|
96
95
|
</label>
|
|
@@ -109,12 +108,14 @@
|
|
|
109
108
|
/>
|
|
110
109
|
%
|
|
111
110
|
</label>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
|
|
111
|
+
<ImageSelect
|
|
112
|
+
label="Add effect"
|
|
113
|
+
value=""
|
|
114
|
+
placeholder="Add effect…"
|
|
115
|
+
disabled={view.model.readOnly}
|
|
116
|
+
options={LAYER_EFFECT_MENU.map((m) => ({ value: m.kind, label: m.label }))}
|
|
117
|
+
onchange={add}
|
|
118
|
+
/>
|
|
118
119
|
</div>
|
|
119
120
|
{#if view.model.readOnly && view.model.reason}<p class="text-muted-foreground">{view.model.reason}</p>{/if}
|
|
120
121
|
<ul class="flex flex-col gap-1" aria-label="Effects">
|
|
@@ -123,13 +124,11 @@
|
|
|
123
124
|
{@const expanded = open === row.id}
|
|
124
125
|
<li data-effect-id={row.id} class="rounded bg-background/60 ring-1 ring-border">
|
|
125
126
|
<div class="flex items-center gap-1.5 px-1.5 py-1">
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
aria-label="{row.label} on"
|
|
127
|
+
<ImageCheckbox
|
|
128
|
+
label="{row.label} on"
|
|
129
129
|
checked={row.enabled}
|
|
130
130
|
disabled={view.model.readOnly}
|
|
131
|
-
onchange={(
|
|
132
|
-
class="accent-primary"
|
|
131
|
+
onchange={(on) => attempt(() => controller.updateLayerEffect(id, row.id, { enabled: on }))}
|
|
133
132
|
/>
|
|
134
133
|
<button type="button" aria-expanded={expanded} class="flex-1 text-left {row.enabled && view.model.enabled ? '' : 'text-muted-foreground'}" onclick={() => (open = expanded ? null : row.id)}>
|
|
135
134
|
{row.label}
|
|
@@ -142,19 +141,16 @@
|
|
|
142
141
|
{#if c.type === 'range'}
|
|
143
142
|
<label class="flex flex-col gap-0.5">
|
|
144
143
|
<span class="text-muted-foreground">{c.label} {shown(c)}{c.unit ?? ''}</span>
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
aria-label={c.label}
|
|
144
|
+
<ImageRange
|
|
145
|
+
label={c.label}
|
|
148
146
|
min={c.min * k(c)}
|
|
149
147
|
max={c.max * k(c)}
|
|
150
148
|
step={c.step * k(c)}
|
|
151
149
|
value={shown(c)}
|
|
152
150
|
disabled={view.model.readOnly}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
oninput={(e) => change(row.id, c, Number((e.currentTarget as HTMLInputElement).value) / k(c), true)}
|
|
157
|
-
class="accent-primary"
|
|
151
|
+
onstart={() => (commitNext = true)}
|
|
152
|
+
onend={() => (commitNext = true)}
|
|
153
|
+
oninput={(v) => change(row.id, c, v / k(c), true)}
|
|
158
154
|
/>
|
|
159
155
|
</label>
|
|
160
156
|
{:else if c.type === 'color'}
|
|
@@ -164,17 +160,13 @@
|
|
|
164
160
|
</label>
|
|
165
161
|
{:else if c.type === 'checkbox'}
|
|
166
162
|
<label class="flex items-center gap-1.5 text-foreground">
|
|
167
|
-
<
|
|
163
|
+
<ImageCheckbox label={c.label} checked={c.value} disabled={view.model.readOnly} onchange={(on) => change(row.id, c, on)} />
|
|
168
164
|
{c.label}
|
|
169
165
|
</label>
|
|
170
166
|
{:else}
|
|
171
167
|
<label class="flex flex-col gap-0.5">
|
|
172
168
|
<span class="text-muted-foreground">{c.label}</span>
|
|
173
|
-
<
|
|
174
|
-
{#each c.options as o (o.value)}
|
|
175
|
-
<option value={o.value}>{o.label}</option>
|
|
176
|
-
{/each}
|
|
177
|
-
</select>
|
|
169
|
+
<ImageSelect label={c.label} value={String(c.value)} disabled={view.model.readOnly} options={c.options.map((o) => ({ value: String(o.value), label: o.label }))} onchange={(v) => change(row.id, c, v)} />
|
|
178
170
|
</label>
|
|
179
171
|
{/if}
|
|
180
172
|
{/each}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
7
7
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
8
8
|
} from '@pixerate/editor/image-editor';
|
|
9
|
+
import { untrack } from 'svelte';
|
|
9
10
|
import CanvasSizePanel from './CanvasSizePanel.svelte';
|
|
10
11
|
import ControlSlider from './ControlSlider.svelte';
|
|
11
12
|
import LayerEffectsPanel from './LayerEffectsPanel.svelte';
|
|
@@ -23,7 +24,17 @@
|
|
|
23
24
|
|
|
24
25
|
let { controller, panelId, class: className = '' }: Props = $props();
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
let tick = $state(0);
|
|
28
|
+
$effect(() => {
|
|
29
|
+
const c = controller;
|
|
30
|
+
// `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
|
|
31
|
+
return untrack(() => c.subscribe(() => tick++));
|
|
32
|
+
});
|
|
33
|
+
// The controller isn't reactive: re-read the active layer after every change.
|
|
34
|
+
const activeLayer = $derived.by(() => {
|
|
35
|
+
void tick;
|
|
36
|
+
return controller.getActiveLayer();
|
|
37
|
+
});
|
|
27
38
|
let shapeOptions = $state({ ...DEFAULT_SHAPE_TOOL_OPTIONS });
|
|
28
39
|
let typeOptions = $state({ ...DEFAULT_TYPE_TOOL_OPTIONS });
|
|
29
40
|
let selectionOptions = $state({ ...DEFAULT_SELECTION_TOOL_OPTIONS });
|
|
@@ -52,8 +63,8 @@
|
|
|
52
63
|
<ControlSlider
|
|
53
64
|
spec={ADJUSTMENT_LAYER_OPACITY_CONTROL}
|
|
54
65
|
label="Opacity"
|
|
55
|
-
value={
|
|
56
|
-
onchange={(val) => controller.setLayerProperties(activeLayer.id, { opacity: val
|
|
66
|
+
value={activeLayer.opacity}
|
|
67
|
+
onchange={(val) => controller.setLayerProperties(activeLayer.id, { opacity: val })}
|
|
57
68
|
/>
|
|
58
69
|
</div>
|
|
59
70
|
{:else}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
3
|
+
import ImageRange from './controls/ImageRange.svelte';
|
|
4
|
+
import ImageSelect from './controls/ImageSelect.svelte';
|
|
3
5
|
import type {
|
|
4
6
|
ImageEditorController,
|
|
5
7
|
SelectionMask,
|
|
@@ -83,41 +85,32 @@
|
|
|
83
85
|
<span>Tolerance</span>
|
|
84
86
|
<span class="font-mono">{options.tolerance}</span>
|
|
85
87
|
</span>
|
|
86
|
-
<
|
|
87
|
-
type="range"
|
|
88
|
-
min="0"
|
|
89
|
-
max="255"
|
|
90
|
-
step="1"
|
|
91
|
-
aria-label="Tolerance"
|
|
92
|
-
value={options.tolerance}
|
|
93
|
-
oninput={(e) => set({ tolerance: Number((e.currentTarget as HTMLInputElement).value) })}
|
|
94
|
-
class={imageRangeClass}
|
|
95
|
-
/>
|
|
88
|
+
<ImageRange label="Tolerance" min={0} max={255} step={1} value={options.tolerance} oninput={(v) => set({ tolerance: v })} />
|
|
96
89
|
</label>
|
|
97
90
|
<label class="flex items-center gap-2 text-foreground">
|
|
98
|
-
<
|
|
91
|
+
<ImageCheckbox label="Contiguous" checked={options.contiguous} onchange={(on) => set({ contiguous: on })} />
|
|
99
92
|
Contiguous
|
|
100
93
|
</label>
|
|
101
94
|
<label class="flex items-center gap-2 text-foreground">
|
|
102
|
-
<
|
|
95
|
+
<ImageCheckbox label="Sample adjusted image" checked={options.sampleMerged} onchange={(on) => set({ sampleMerged: on })} />
|
|
103
96
|
Sample adjusted image
|
|
104
97
|
</label>
|
|
105
98
|
<label class="flex items-center gap-2 text-foreground">
|
|
106
99
|
Sample
|
|
107
|
-
<
|
|
108
|
-
|
|
100
|
+
<ImageSelect
|
|
101
|
+
label="Sample from"
|
|
109
102
|
value={options.sample ?? 'composite'}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
options={[
|
|
104
|
+
{ value: 'composite', label: 'All layers' },
|
|
105
|
+
{ value: 'active-layer', label: 'Active layer' },
|
|
106
|
+
]}
|
|
107
|
+
onchange={(v) => set({ sample: v as 'composite' | 'active-layer' })}
|
|
108
|
+
/>
|
|
116
109
|
</label>
|
|
117
110
|
</div>
|
|
118
111
|
{/if}
|
|
119
112
|
<label class="flex items-center gap-2 text-foreground">
|
|
120
|
-
<
|
|
113
|
+
<ImageCheckbox label="Anti-alias" checked={options.antiAlias} onchange={(on) => set({ antiAlias: on })} />
|
|
121
114
|
Anti-alias
|
|
122
115
|
</label>
|
|
123
116
|
<div class="flex flex-wrap gap-1">
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ImageCheckbox from './controls/ImageCheckbox.svelte';
|
|
3
|
+
import ImageRange from './controls/ImageRange.svelte';
|
|
2
4
|
import { imageChipClass, imageColorInputClass, imageFieldClass } from './styles.js';
|
|
3
5
|
import type { ImageEditorController, ShapeToolOptions } from '@pixerate/editor/image-editor';
|
|
4
6
|
import { LIVE_SHAPE_KINDS, SHAPE_KIND_LABELS, applyShapeOptions } from '@pixerate/editor/image-editor';
|
|
@@ -44,7 +46,6 @@
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
const value = (e: Event) => (e.target as HTMLInputElement).value;
|
|
47
|
-
const checked = (e: Event) => (e.target as HTMLInputElement).checked;
|
|
48
49
|
</script>
|
|
49
50
|
|
|
50
51
|
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="shape-options">
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
</div>
|
|
56
57
|
<div class="flex flex-wrap items-center gap-3">
|
|
57
58
|
<label class="flex items-center gap-1">
|
|
58
|
-
<
|
|
59
|
+
<ImageCheckbox label="Fill" checked={options.fill !== null} onchange={(on) => set({ fill: on ? '#4f46e5' : null })} />
|
|
59
60
|
Fill
|
|
60
61
|
<input
|
|
61
62
|
type="color"
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
/>
|
|
68
69
|
</label>
|
|
69
70
|
<label class="flex items-center gap-1">
|
|
70
|
-
<
|
|
71
|
+
<ImageCheckbox label="Stroke" checked={options.stroke !== null} onchange={(on) => set({ stroke: on ? '#000000' : null })} />
|
|
71
72
|
Stroke
|
|
72
73
|
<input
|
|
73
74
|
type="color"
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
</label>
|
|
102
103
|
<label class="flex items-center gap-1">
|
|
103
104
|
Inner
|
|
104
|
-
<
|
|
105
|
+
<ImageRange label="Inner radius" min={0.05} max={1} step={0.05} value={options.innerRatio} oninput={(v) => set({ innerRatio: v })} />
|
|
105
106
|
</label>
|
|
106
107
|
{:else if options.kind === 'line'}
|
|
107
108
|
<label class="flex items-center gap-1">
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
<input type="number" aria-label="Line weight" min="1" max="200" value={options.weight} oninput={(e) => set({ weight: num(value(e), 1, 200) })} class="w-14 {imageFieldClass}" />
|
|
110
111
|
</label>
|
|
111
112
|
<label class="flex items-center gap-1">
|
|
112
|
-
<
|
|
113
|
+
<ImageCheckbox label="Arrowhead" checked={options.arrow} onchange={(on) => set({ arrow: on })} />
|
|
113
114
|
Arrowhead
|
|
114
115
|
</label>
|
|
115
116
|
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getImageEditorControls, type ImageCheckboxProps } from './controls.js';
|
|
3
|
+
|
|
4
|
+
let { checked, label, disabled = false, onchange, class: className = '' }: ImageCheckboxProps = $props();
|
|
5
|
+
const Host = getImageEditorControls().Checkbox;
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
{#if Host}
|
|
9
|
+
<Host {checked} {label} {disabled} {onchange} class={className} />
|
|
10
|
+
{:else}
|
|
11
|
+
<input
|
|
12
|
+
type="checkbox"
|
|
13
|
+
aria-label={label}
|
|
14
|
+
{checked}
|
|
15
|
+
{disabled}
|
|
16
|
+
onchange={(e) => onchange((e.currentTarget as HTMLInputElement).checked)}
|
|
17
|
+
class="accent-primary {className}"
|
|
18
|
+
/>
|
|
19
|
+
{/if}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getImageEditorControls, type ImageRangeProps } from './controls.js';
|
|
3
|
+
|
|
4
|
+
let { value, min, max, step, label, disabled = false, oninput, onstart, onend, class: className = '' }: ImageRangeProps = $props();
|
|
5
|
+
const Host = getImageEditorControls().Range;
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
{#if Host}
|
|
9
|
+
<Host {value} {min} {max} {step} {label} {disabled} {oninput} {onstart} {onend} class={className} />
|
|
10
|
+
{:else}
|
|
11
|
+
<input
|
|
12
|
+
type="range"
|
|
13
|
+
aria-label={label}
|
|
14
|
+
{min}
|
|
15
|
+
{max}
|
|
16
|
+
{step}
|
|
17
|
+
{value}
|
|
18
|
+
{disabled}
|
|
19
|
+
onpointerdown={() => onstart?.()}
|
|
20
|
+
onpointerup={() => onend?.()}
|
|
21
|
+
onkeydown={() => onstart?.()}
|
|
22
|
+
onkeyup={() => onend?.()}
|
|
23
|
+
oninput={(e) => oninput(Number((e.currentTarget as HTMLInputElement).value))}
|
|
24
|
+
class="w-full accent-primary disabled:opacity-40 {className}"
|
|
25
|
+
/>
|
|
26
|
+
{/if}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { imageSelectClass } from '../styles.js';
|
|
3
|
+
import { getImageEditorControls, type ImageSelectProps } from './controls.js';
|
|
4
|
+
|
|
5
|
+
let { value, options, label, placeholder, disabled = false, onchange, class: className = '' }: ImageSelectProps = $props();
|
|
6
|
+
const Host = getImageEditorControls().Select;
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
{#if Host}
|
|
10
|
+
<Host {value} {options} {label} {placeholder} {disabled} {onchange} class={className} />
|
|
11
|
+
{:else}
|
|
12
|
+
<select
|
|
13
|
+
aria-label={label}
|
|
14
|
+
{value}
|
|
15
|
+
{disabled}
|
|
16
|
+
onchange={(e) => {
|
|
17
|
+
const select = e.currentTarget as HTMLSelectElement;
|
|
18
|
+
onchange(select.value);
|
|
19
|
+
// A placeholder select (an action menu) shows its placeholder again.
|
|
20
|
+
if (placeholder !== undefined) select.value = value;
|
|
21
|
+
}}
|
|
22
|
+
class="{imageSelectClass} {className}"
|
|
23
|
+
>
|
|
24
|
+
{#if placeholder !== undefined}<option value="">{placeholder}</option>{/if}
|
|
25
|
+
{#each options as o (o.value)}
|
|
26
|
+
<option value={o.value}>{o.label}</option>
|
|
27
|
+
{/each}
|
|
28
|
+
</select>
|
|
29
|
+
{/if}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Component } from 'svelte';
|
|
2
|
+
/** A checkbox in the image editor's panels. */
|
|
3
|
+
export interface ImageCheckboxProps {
|
|
4
|
+
checked: boolean;
|
|
5
|
+
/** The accessible name. */
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
onchange: (checked: boolean) => void;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A range slider in the image editor's panels. */
|
|
12
|
+
export interface ImageRangeProps {
|
|
13
|
+
value: number;
|
|
14
|
+
min: number;
|
|
15
|
+
max: number;
|
|
16
|
+
step: number;
|
|
17
|
+
/** The accessible name. */
|
|
18
|
+
label: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Every change while the value moves. */
|
|
21
|
+
oninput: (value: number) => void;
|
|
22
|
+
/** A drag (or key press) starts. */
|
|
23
|
+
onstart?: () => void;
|
|
24
|
+
/** A drag (or key press) ends. */
|
|
25
|
+
onend?: () => void;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
/** One choice of an {@link ImageSelectProps}. */
|
|
29
|
+
export interface ImageSelectOption {
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}
|
|
33
|
+
/** A dropdown in the image editor's panels. */
|
|
34
|
+
export interface ImageSelectProps {
|
|
35
|
+
value: string;
|
|
36
|
+
options: readonly ImageSelectOption[];
|
|
37
|
+
/** The accessible name. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Shown when `value` matches no option (e.g. "Add effect…"). */
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
onchange: (value: string) => void;
|
|
43
|
+
class?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The host's own form controls for the image editor's panels. Any left out fall back to the
|
|
47
|
+
* editor's plain inputs.
|
|
48
|
+
*/
|
|
49
|
+
export interface ImageEditorControls {
|
|
50
|
+
Checkbox?: Component<ImageCheckboxProps>;
|
|
51
|
+
Range?: Component<ImageRangeProps>;
|
|
52
|
+
Select?: Component<ImageSelectProps>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Makes the image editor's panels below this component use the host's controls (call it while
|
|
56
|
+
* a component initialises, like `setContext`).
|
|
57
|
+
*/
|
|
58
|
+
export declare function setImageEditorControls(controls: ImageEditorControls): ImageEditorControls;
|
|
59
|
+
/** The controls set by an ancestor with {@link setImageEditorControls}, or none. */
|
|
60
|
+
export declare function getImageEditorControls(): ImageEditorControls;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('pixerate.image-editor.controls');
|
|
3
|
+
/**
|
|
4
|
+
* Makes the image editor's panels below this component use the host's controls (call it while
|
|
5
|
+
* a component initialises, like `setContext`).
|
|
6
|
+
*/
|
|
7
|
+
export function setImageEditorControls(controls) {
|
|
8
|
+
return setContext(KEY, controls);
|
|
9
|
+
}
|
|
10
|
+
/** The controls set by an ancestor with {@link setImageEditorControls}, or none. */
|
|
11
|
+
export function getImageEditorControls() {
|
|
12
|
+
return getContext(KEY) ?? {};
|
|
13
|
+
}
|
|
@@ -40,6 +40,10 @@ export { default as ShapeOptionsBar } from './ShapeOptionsBar.svelte';
|
|
|
40
40
|
export { default as TypeOptionsBar } from './TypeOptionsBar.svelte';
|
|
41
41
|
export { default as ImageProviderEffectsPanel } from './ImageProviderEffectsPanel.svelte';
|
|
42
42
|
export * from './imageEditorState.svelte.js';
|
|
43
|
+
export { default as ImageCheckbox } from './controls/ImageCheckbox.svelte';
|
|
44
|
+
export { default as ImageRange } from './controls/ImageRange.svelte';
|
|
45
|
+
export { default as ImageSelect } from './controls/ImageSelect.svelte';
|
|
46
|
+
export { setImageEditorControls, getImageEditorControls, type ImageEditorControls, type ImageCheckboxProps, type ImageRangeProps, type ImageSelectProps, type ImageSelectOption, } from './controls/controls.js';
|
|
43
47
|
export type ImageEditorProps = ComponentProps<typeof ImageEditorComponent>;
|
|
44
48
|
export type ImageEditorCanvasProps = ComponentProps<typeof ImageEditorCanvasComponent>;
|
|
45
49
|
export type CanvasSizePanelProps = ComponentProps<typeof CanvasSizePanelComponent>;
|
|
@@ -19,4 +19,8 @@ export { default as ShapeOptionsBar } from './ShapeOptionsBar.svelte';
|
|
|
19
19
|
export { default as TypeOptionsBar } from './TypeOptionsBar.svelte';
|
|
20
20
|
export { default as ImageProviderEffectsPanel } from './ImageProviderEffectsPanel.svelte';
|
|
21
21
|
export * from './imageEditorState.svelte.js';
|
|
22
|
+
export { default as ImageCheckbox } from './controls/ImageCheckbox.svelte';
|
|
23
|
+
export { default as ImageRange } from './controls/ImageRange.svelte';
|
|
24
|
+
export { default as ImageSelect } from './controls/ImageSelect.svelte';
|
|
25
|
+
export { setImageEditorControls, getImageEditorControls, } from './controls/controls.js';
|
|
22
26
|
export { applyRegionMosaic, applyRegionBlur, resolveMosaicRegionBounds } from '@pixerate/editor/image-editor';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"@tiptap/extension-bubble-menu": "^2.27.3",
|
|
169
169
|
"@tiptap/extension-placeholder": "^2.27.3",
|
|
170
170
|
"@tiptap/starter-kit": "^2.27.3",
|
|
171
|
-
"@pixerate/editor": "0.
|
|
171
|
+
"@pixerate/editor": "0.169.0"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
174
|
"@dagrejs/dagre": "^1.1.4",
|