@pixerate/editor-svelte 0.108.0 → 0.109.1
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/EditableTextNodeEditor.svelte +6 -0
- package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +8 -2
- package/dist/canvas/createCanvasPlaybackSurface.svelte.js +13 -8
- package/dist/image-editor/AdjustmentLayersPanel.svelte +27 -26
- package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +2 -0
- package/dist/image-editor/BrushOptionsBar.svelte +35 -34
- package/dist/image-editor/ColorProfilePanel.svelte +12 -12
- package/dist/image-editor/ControlSlider.svelte +6 -4
- package/dist/image-editor/ControlSlider.svelte.d.ts +2 -0
- package/dist/image-editor/CurvesEditor.svelte +6 -4
- package/dist/image-editor/CurvesEditor.svelte.d.ts +2 -0
- package/dist/image-editor/FreeTransformOptionsBar.svelte +22 -21
- package/dist/image-editor/Histogram.svelte +1 -1
- package/dist/image-editor/ImageEditor.svelte +127 -142
- package/dist/image-editor/ImageEffectControls.svelte +15 -11
- package/dist/image-editor/ImageEffectControls.svelte.d.ts +2 -0
- package/dist/image-editor/ImageProviderEffectsPanel.svelte +15 -15
- package/dist/image-editor/LayerEffectsPanel.svelte +22 -23
- package/dist/image-editor/LayersPanel.svelte +39 -39
- package/dist/image-editor/RefineEdgePanel.svelte +14 -13
- package/dist/image-editor/SelectionOptionsBar.svelte +12 -11
- package/dist/image-editor/ShapeOptionsBar.svelte +14 -13
- package/dist/image-editor/TypeOptionsBar.svelte +12 -11
- package/dist/image-editor/styles.d.ts +51 -0
- package/dist/image-editor/styles.js +51 -0
- package/dist/playback/playback.svelte.d.ts +7 -4
- package/dist/playback/playback.svelte.js +17 -9
- package/package.json +2 -2
|
@@ -127,6 +127,12 @@
|
|
|
127
127
|
</div>
|
|
128
128
|
|
|
129
129
|
<style>
|
|
130
|
+
/* Typography gives bold, italic and code their own colour, which would override a colour mark
|
|
131
|
+
(or gradient text) on the text around them. Let them inherit it. */
|
|
132
|
+
:global(.pixerate-editor-content :is(strong, b, em, i, u, s, a, code)) {
|
|
133
|
+
color: inherit;
|
|
134
|
+
}
|
|
135
|
+
|
|
130
136
|
:global(.pixerate-editor-content .is-empty::before) {
|
|
131
137
|
content: attr(data-placeholder);
|
|
132
138
|
float: left;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { type CanvasEdge, type CanvasNode, type NodeRectOptions } from '@pixerate/editor/canvas';
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentPlayback, type PlaybackChromeOptions, type PresenceStore } from '@pixerate/editor/tools';
|
|
3
3
|
type MaybeGetter<T> = T | (() => T);
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `locate`, `locateElement` and `open` (see `PlaybackChromeOptions`) let the host's own chrome over
|
|
6
|
+
* the canvas (a HUD, toolbar or panels) answer refs the canvas does not find, such as
|
|
7
|
+
* `{ kind: 'panel' | 'control', id }`: `locate` in flow coordinates, `locateElement` as an element
|
|
8
|
+
* whose client box is mapped into flow coordinates through the viewport.
|
|
9
|
+
*/
|
|
10
|
+
export interface CreateCanvasPlaybackSurfaceOptions<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge> extends PlaybackChromeOptions {
|
|
5
11
|
/** The flow's nodes and edges now (e.g. the `bind:nodes` state). */
|
|
6
12
|
getNodes: () => readonly TNode[];
|
|
7
13
|
getEdges?: () => readonly TEdge[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCanvasPlaybackLocator } from '@pixerate/editor/canvas';
|
|
2
|
+
import { locatePlaybackChrome } from '@pixerate/editor/tools';
|
|
2
3
|
import { createPlaybackSurface } from '../playback/playback.svelte.js';
|
|
3
4
|
/**
|
|
4
5
|
* Svelte 5 rune: attaches the node canvas as an agent playback surface under `id` (the canvas tool
|
|
@@ -7,15 +8,20 @@ import { createPlaybackSurface } from '../playback/playback.svelte.js';
|
|
|
7
8
|
* React's `useCanvasPlaybackSurface`.
|
|
8
9
|
*/
|
|
9
10
|
export function createCanvasPlaybackSurface(playback, id, options) {
|
|
10
|
-
const
|
|
11
|
+
const locateFlow = createCanvasPlaybackLocator({
|
|
11
12
|
getNodes: options.getNodes,
|
|
12
13
|
getEdges: () => options.getEdges?.() ?? [],
|
|
13
14
|
rectOptions: options.rectOptions
|
|
14
15
|
});
|
|
15
16
|
const box = () => options.container()?.getBoundingClientRect() ?? null;
|
|
17
|
+
const fromViewport = (p) => {
|
|
18
|
+
const r = box();
|
|
19
|
+
const { x, y, zoom } = options.getViewport();
|
|
20
|
+
return { x: (p.x - (r?.left ?? 0) - x) / zoom, y: (p.y - (r?.top ?? 0) - y) / zoom };
|
|
21
|
+
};
|
|
16
22
|
return createPlaybackSurface(playback, id, {
|
|
17
23
|
defaultKind: 'node',
|
|
18
|
-
locate,
|
|
24
|
+
locate: (ref) => locateFlow(ref) ?? locatePlaybackChrome(ref, options, fromViewport),
|
|
19
25
|
bounds: () => {
|
|
20
26
|
const r = box();
|
|
21
27
|
const { x, y, zoom } = options.getViewport();
|
|
@@ -33,11 +39,10 @@ export function createCanvasPlaybackSurface(playback, id, options) {
|
|
|
33
39
|
const { x, y, zoom } = options.getViewport();
|
|
34
40
|
return { x: (r?.left ?? 0) + p.x * zoom + x, y: (r?.top ?? 0) + p.y * zoom + y };
|
|
35
41
|
},
|
|
36
|
-
fromViewport
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
reveal: () => options.reveal?.()
|
|
42
|
+
fromViewport,
|
|
43
|
+
reveal: () => options.reveal?.(),
|
|
44
|
+
get open() {
|
|
45
|
+
return options.open ? (ref) => options.open?.(ref) : undefined;
|
|
46
|
+
}
|
|
42
47
|
});
|
|
43
48
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageSectionTitleClass, imageSelectClass } from './styles.js';
|
|
2
3
|
import type {
|
|
3
4
|
AdjustmentKind,
|
|
4
5
|
AdjustmentLayer,
|
|
@@ -28,9 +29,11 @@
|
|
|
28
29
|
/** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
|
|
29
30
|
layers?: AdjustmentLayer[];
|
|
30
31
|
histogram?: ImageHistogram | null;
|
|
32
|
+
/** Extra classes on the root element. */
|
|
33
|
+
class?: string;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
let { controller, layers: layersProp, histogram = null }: Props = $props();
|
|
36
|
+
let { controller, layers: layersProp, histogram = null, class: className = '' }: Props = $props();
|
|
34
37
|
|
|
35
38
|
// Follows the controller when no `layers` prop is given.
|
|
36
39
|
let tick = $state(0);
|
|
@@ -66,10 +69,8 @@
|
|
|
66
69
|
|
|
67
70
|
const selected = $derived(layers.find((l) => l.id === selectedId) ?? null);
|
|
68
71
|
const topFirst = $derived([...layers].reverse());
|
|
69
|
-
|
|
70
|
-
const selectClass = 'bg-slate-800 border border-slate-700 rounded px-1.5 py-1 text-slate-200';
|
|
71
72
|
const chipClass = (on: boolean) =>
|
|
72
|
-
`px-2 py-0.5 rounded text-[10px] ${on ? 'bg-
|
|
73
|
+
`px-2 py-0.5 rounded text-[10px] ${on ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:text-foreground'}`;
|
|
73
74
|
|
|
74
75
|
function setParam(layer: AdjustmentLayer, path: string, value: unknown, commit = true) {
|
|
75
76
|
controller.updateAdjustmentLayer(layer.id, { params: setControlPath(layer.params, path, value) as never }, commit);
|
|
@@ -103,17 +104,17 @@
|
|
|
103
104
|
}
|
|
104
105
|
</script>
|
|
105
106
|
|
|
106
|
-
<div class="flex flex-col gap-2 text-xs" data-testid="adjustment-layers-panel">
|
|
107
|
+
<div class="flex flex-col gap-2 text-xs {className}" data-testid="adjustment-layers-panel">
|
|
107
108
|
<div class="flex items-center justify-between">
|
|
108
|
-
<span class=
|
|
109
|
+
<span class={imageSectionTitleClass}>Adjustment Layers</span>
|
|
109
110
|
</div>
|
|
110
111
|
<div class="flex gap-1.5">
|
|
111
|
-
<select aria-label="New adjustment layer kind" class="{
|
|
112
|
+
<select aria-label="New adjustment layer kind" class="{imageSelectClass} flex-1" bind:value={newKind}>
|
|
112
113
|
{#each ADJUSTMENT_KINDS as k (k.kind)}
|
|
113
114
|
<option value={k.kind}>{k.label}</option>
|
|
114
115
|
{/each}
|
|
115
116
|
</select>
|
|
116
|
-
<button type="button" class="px-2 py-1 rounded bg-
|
|
117
|
+
<button type="button" class="px-2 py-1 rounded bg-primary text-primary-foreground hover:bg-primary/90" onclick={addLayer}>
|
|
117
118
|
Add Layer
|
|
118
119
|
</button>
|
|
119
120
|
</div>
|
|
@@ -125,19 +126,19 @@
|
|
|
125
126
|
{@const label = layer.name || adjustmentKindLabel(layer.kind)}
|
|
126
127
|
<li
|
|
127
128
|
class="flex items-center gap-1.5 rounded px-1.5 py-1 {layer.id === selectedId
|
|
128
|
-
? 'bg-
|
|
129
|
-
: 'bg-
|
|
129
|
+
? 'bg-primary/20 ring-1 ring-primary/50'
|
|
130
|
+
: 'bg-muted/60'}"
|
|
130
131
|
>
|
|
131
132
|
<input
|
|
132
133
|
type="checkbox"
|
|
133
134
|
aria-label="Show {label}"
|
|
134
135
|
checked={layer.enabled}
|
|
135
136
|
onchange={() => controller.toggleAdjustmentLayer(layer.id)}
|
|
136
|
-
class="accent-
|
|
137
|
+
class="accent-primary"
|
|
137
138
|
/>
|
|
138
139
|
<button
|
|
139
140
|
type="button"
|
|
140
|
-
class="flex-1 text-left truncate {layer.enabled ? 'text-
|
|
141
|
+
class="flex-1 text-left truncate {layer.enabled ? 'text-foreground' : 'text-muted-foreground line-through'}"
|
|
141
142
|
aria-pressed={layer.id === selectedId}
|
|
142
143
|
onclick={() => (selectedId = layer.id === selectedId ? null : layer.id)}
|
|
143
144
|
>
|
|
@@ -148,7 +149,7 @@
|
|
|
148
149
|
aria-label="Move {label} up"
|
|
149
150
|
disabled={index === layers.length - 1}
|
|
150
151
|
onclick={() => controller.moveAdjustmentLayer(layer.id, index + 1)}
|
|
151
|
-
class="px-1 text-
|
|
152
|
+
class="px-1 text-muted-foreground hover:text-foreground disabled:opacity-30"
|
|
152
153
|
>
|
|
153
154
|
↑
|
|
154
155
|
</button>
|
|
@@ -157,7 +158,7 @@
|
|
|
157
158
|
aria-label="Move {label} down"
|
|
158
159
|
disabled={index === 0}
|
|
159
160
|
onclick={() => controller.moveAdjustmentLayer(layer.id, index - 1)}
|
|
160
|
-
class="px-1 text-
|
|
161
|
+
class="px-1 text-muted-foreground hover:text-foreground disabled:opacity-30"
|
|
161
162
|
>
|
|
162
163
|
↓
|
|
163
164
|
</button>
|
|
@@ -168,7 +169,7 @@
|
|
|
168
169
|
controller.removeAdjustmentLayer(layer.id);
|
|
169
170
|
if (selectedId === layer.id) selectedId = null;
|
|
170
171
|
}}
|
|
171
|
-
class="px-1 text-
|
|
172
|
+
class="px-1 text-destructive hover:text-destructive"
|
|
172
173
|
>
|
|
173
174
|
✕
|
|
174
175
|
</button>
|
|
@@ -179,12 +180,12 @@
|
|
|
179
180
|
|
|
180
181
|
{#if selected}
|
|
181
182
|
{@const layer = selected}
|
|
182
|
-
<div class="flex flex-col gap-2 rounded border border-
|
|
183
|
+
<div class="flex flex-col gap-2 rounded border border-border p-2" data-testid="adjustment-layer-editor">
|
|
183
184
|
<div class="flex items-center gap-2">
|
|
184
|
-
<span class="text-
|
|
185
|
+
<span class="text-muted-foreground">Blend</span>
|
|
185
186
|
<select
|
|
186
187
|
aria-label="Blend mode"
|
|
187
|
-
class="{
|
|
188
|
+
class="{imageSelectClass} flex-1"
|
|
188
189
|
value={layer.blend}
|
|
189
190
|
onchange={(e) => controller.updateAdjustmentLayer(layer.id, { blend: (e.target as HTMLSelectElement).value as BlendMode })}
|
|
190
191
|
>
|
|
@@ -244,7 +245,7 @@
|
|
|
244
245
|
{/each}
|
|
245
246
|
</div>
|
|
246
247
|
{:else if layer.kind === 'photoFilter'}
|
|
247
|
-
<label class="flex items-center gap-2 text-
|
|
248
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
248
249
|
Filter colour
|
|
249
250
|
<input
|
|
250
251
|
type="color"
|
|
@@ -255,7 +256,7 @@
|
|
|
255
256
|
/>
|
|
256
257
|
</label>
|
|
257
258
|
{:else if layer.kind === 'invert'}
|
|
258
|
-
<p class="text-
|
|
259
|
+
<p class="text-muted-foreground text-[11px]">Invert has no settings.</p>
|
|
259
260
|
{/if}
|
|
260
261
|
|
|
261
262
|
{#each visibleControls(layer) as spec (spec.id)}
|
|
@@ -263,34 +264,34 @@
|
|
|
263
264
|
{/each}
|
|
264
265
|
|
|
265
266
|
{#if layer.kind === 'hueSaturation' && range === -1}
|
|
266
|
-
<label class="flex items-center gap-2 text-
|
|
267
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
267
268
|
<input
|
|
268
269
|
type="checkbox"
|
|
269
270
|
checked={layer.params.colorize}
|
|
270
271
|
onchange={(e) => setParam(layer, 'colorize', (e.target as HTMLInputElement).checked)}
|
|
271
|
-
class="accent-
|
|
272
|
+
class="accent-primary"
|
|
272
273
|
/>
|
|
273
274
|
Colorize
|
|
274
275
|
</label>
|
|
275
276
|
{/if}
|
|
276
277
|
{#if layer.kind === 'colorBalance' || layer.kind === 'photoFilter'}
|
|
277
|
-
<label class="flex items-center gap-2 text-
|
|
278
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
278
279
|
<input
|
|
279
280
|
type="checkbox"
|
|
280
281
|
checked={layer.params.preserveLuminosity}
|
|
281
282
|
onchange={(e) => setParam(layer, 'preserveLuminosity', (e.target as HTMLInputElement).checked)}
|
|
282
|
-
class="accent-
|
|
283
|
+
class="accent-primary"
|
|
283
284
|
/>
|
|
284
285
|
Preserve luminosity
|
|
285
286
|
</label>
|
|
286
287
|
{/if}
|
|
287
288
|
{#if layer.kind === 'blackWhite'}
|
|
288
|
-
<label class="flex items-center gap-2 text-
|
|
289
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
289
290
|
<input
|
|
290
291
|
type="checkbox"
|
|
291
292
|
checked={layer.params.tint !== null}
|
|
292
293
|
onchange={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).checked ? '#e1d3b3' : null)}
|
|
293
|
-
class="accent-
|
|
294
|
+
class="accent-primary"
|
|
294
295
|
/>
|
|
295
296
|
Tint
|
|
296
297
|
{#if layer.params.tint}
|
|
@@ -4,6 +4,8 @@ interface Props {
|
|
|
4
4
|
/** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
|
|
5
5
|
layers?: AdjustmentLayer[];
|
|
6
6
|
histogram?: ImageHistogram | null;
|
|
7
|
+
/** Extra classes on the root element. */
|
|
8
|
+
class?: string;
|
|
7
9
|
}
|
|
8
10
|
declare const AdjustmentLayersPanel: import("svelte").Component<Props, {}, "">;
|
|
9
11
|
type AdjustmentLayersPanel = ReturnType<typeof AdjustmentLayersPanel>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageChipClass, imageColorInputClass } from './styles.js';
|
|
2
3
|
import type { ImageEditorController } from '@pixerate/editor/image-editor';
|
|
3
4
|
import {
|
|
4
5
|
BRUSH_OPTION_CONTROLS,
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
});
|
|
42
43
|
let error = $state<string | null>(null);
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
|
|
46
47
|
function attempt(fn: () => unknown) {
|
|
47
48
|
try {
|
|
@@ -53,14 +54,14 @@
|
|
|
53
54
|
}
|
|
54
55
|
</script>
|
|
55
56
|
|
|
56
|
-
<div class="flex flex-col gap-2 text-xs text-
|
|
57
|
+
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="brush-options">
|
|
57
58
|
<div class="flex flex-wrap gap-1" role="toolbar" aria-label="Paint tools">
|
|
58
59
|
{#each PAINT_TOOLS as t (t.id)}
|
|
59
60
|
<button
|
|
60
61
|
type="button"
|
|
61
62
|
aria-pressed={view.tool === t.id}
|
|
62
63
|
title={t.shortcut ? `${t.label} (${t.shortcut})` : t.label}
|
|
63
|
-
class={
|
|
64
|
+
class={imageChipClass(view.tool === t.id)}
|
|
64
65
|
onclick={() => controller.setTool(t.id)}>{t.label}</button>
|
|
65
66
|
{/each}
|
|
66
67
|
</div>
|
|
@@ -72,9 +73,9 @@
|
|
|
72
73
|
aria-label="Foreground colour"
|
|
73
74
|
value={view.foreground}
|
|
74
75
|
oninput={(e) => controller.setPaintOptions({ foreground: (e.target as HTMLInputElement).value })}
|
|
75
|
-
class=
|
|
76
|
+
class={imageColorInputClass}
|
|
76
77
|
/>
|
|
77
|
-
<span class="text-
|
|
78
|
+
<span class="text-muted-foreground">FG</span>
|
|
78
79
|
</label>
|
|
79
80
|
<label class="flex items-center gap-1">
|
|
80
81
|
<input
|
|
@@ -82,18 +83,18 @@
|
|
|
82
83
|
aria-label="Background colour"
|
|
83
84
|
value={view.background}
|
|
84
85
|
oninput={(e) => controller.setPaintOptions({ background: (e.target as HTMLInputElement).value })}
|
|
85
|
-
class=
|
|
86
|
+
class={imageColorInputClass}
|
|
86
87
|
/>
|
|
87
|
-
<span class="text-
|
|
88
|
+
<span class="text-muted-foreground">BG</span>
|
|
88
89
|
</label>
|
|
89
|
-
<button type="button" title="Swap colours (X)" class={
|
|
90
|
-
<button type="button" title="Default colours (D)" class={
|
|
90
|
+
<button type="button" title="Swap colours (X)" class={imageChipClass(false)} onclick={() => controller.swapPaintColors()}>Swap</button>
|
|
91
|
+
<button type="button" title="Default colours (D)" class={imageChipClass(false)} onclick={() => controller.resetPaintColors()}>Default</button>
|
|
91
92
|
{#if view.tool === 'brush'}
|
|
92
93
|
<button
|
|
93
94
|
type="button"
|
|
94
95
|
aria-pressed={view.paint.type === 'gradient'}
|
|
95
96
|
title="Gradient paint: the brush changes colour as the stroke goes"
|
|
96
|
-
class={
|
|
97
|
+
class={imageChipClass(view.paint.type === 'gradient')}
|
|
97
98
|
onclick={() => attempt(() => controller.setPaintOptions(view.paint.type === 'gradient' ? { foreground: view.foreground } : { paint: DEFAULT_PAINT_GRADIENT }))}>Gradient</button
|
|
98
99
|
>
|
|
99
100
|
{/if}
|
|
@@ -108,16 +109,16 @@
|
|
|
108
109
|
|
|
109
110
|
{#if view.tool === 'mixer'}
|
|
110
111
|
{@const load = view.mixerLoad}
|
|
111
|
-
<div class="flex flex-col gap-2 border-t border-
|
|
112
|
+
<div class="flex flex-col gap-2 border-t border-border pt-2" role="group" aria-label="Mixer Brush" data-testid="mixer-options">
|
|
112
113
|
<div class="flex items-center gap-2">
|
|
113
114
|
<span
|
|
114
115
|
aria-label="Brush load"
|
|
115
116
|
title={load.reservoir ? `Loaded ${rgbUnitToHex(load.reservoir)}, ${Math.round(load.amount * 100)}% left` : 'Unloaded'}
|
|
116
|
-
class="h-6 w-6 rounded border border-
|
|
117
|
+
class="h-6 w-6 rounded border border-border"
|
|
117
118
|
style:background={load.reservoir ? rgbUnitToHex(load.reservoir) : 'transparent'}
|
|
118
119
|
></span>
|
|
119
|
-
<button type="button" title="Fill the reservoir with the foreground colour" class={
|
|
120
|
-
<button type="button" title="Empty the picked-up colour" class={
|
|
120
|
+
<button type="button" title="Fill the reservoir with the foreground colour" class={imageChipClass(false)} onclick={() => controller.loadMixerBrush()}>Load brush</button>
|
|
121
|
+
<button type="button" title="Empty the picked-up colour" class={imageChipClass(false)} onclick={() => controller.cleanMixerBrush()}>Clean brush</button>
|
|
121
122
|
</div>
|
|
122
123
|
<div class="flex gap-1" role="group" aria-label="Mixing">
|
|
123
124
|
{#each MIXER_MIXING_OPTIONS as m (m.id)}
|
|
@@ -125,21 +126,21 @@
|
|
|
125
126
|
type="button"
|
|
126
127
|
aria-pressed={view.mixer.mixing === m.id}
|
|
127
128
|
title={m.title}
|
|
128
|
-
class={
|
|
129
|
+
class={imageChipClass(view.mixer.mixing === m.id)}
|
|
129
130
|
onclick={() => controller.setPaintOptions({ mixer: { mixing: m.id } })}>{m.label}</button>
|
|
130
131
|
{/each}
|
|
131
132
|
</div>
|
|
132
133
|
{#each MIXER_OPTION_CONTROLS as spec (spec.id)}
|
|
133
134
|
<ControlSlider {spec} value={view.mixer[spec.id]} onchange={(v) => controller.setPaintOptions({ mixer: { [spec.id]: v } })} />
|
|
134
135
|
{/each}
|
|
135
|
-
<div class="flex flex-wrap gap-3 text-
|
|
136
|
+
<div class="flex flex-wrap gap-3 text-foreground">
|
|
136
137
|
<label class="flex items-center gap-1.5">
|
|
137
138
|
<input
|
|
138
139
|
type="checkbox"
|
|
139
140
|
aria-label="Load the brush after each stroke"
|
|
140
141
|
checked={view.mixer.loadEachStroke}
|
|
141
142
|
onchange={(e) => controller.setPaintOptions({ mixer: { loadEachStroke: (e.target as HTMLInputElement).checked } })}
|
|
142
|
-
class="accent-
|
|
143
|
+
class="accent-primary"
|
|
143
144
|
/>
|
|
144
145
|
Load each stroke
|
|
145
146
|
</label>
|
|
@@ -149,7 +150,7 @@
|
|
|
149
150
|
aria-label="Clean the brush after each stroke"
|
|
150
151
|
checked={view.mixer.cleanEachStroke}
|
|
151
152
|
onchange={(e) => controller.setPaintOptions({ mixer: { cleanEachStroke: (e.target as HTMLInputElement).checked } })}
|
|
152
|
-
class="accent-
|
|
153
|
+
class="accent-primary"
|
|
153
154
|
/>
|
|
154
155
|
Clean each stroke
|
|
155
156
|
</label>
|
|
@@ -158,12 +159,12 @@
|
|
|
158
159
|
{/if}
|
|
159
160
|
|
|
160
161
|
{#if isRetouchPaintTool(view.tool)}
|
|
161
|
-
<div class="flex flex-col gap-2 border-t border-
|
|
162
|
+
<div class="flex flex-col gap-2 border-t border-border pt-2" role="group" aria-label="Retouch options" data-testid="retouch-options">
|
|
162
163
|
{#if view.tool === 'clone' || view.tool === 'heal'}
|
|
163
164
|
<div class="flex items-center gap-2">
|
|
164
|
-
<span class="text-
|
|
165
|
+
<span class="text-muted-foreground" data-testid="clone-source">{view.sourceLabel}</span>
|
|
165
166
|
{#if view.cloneSource}
|
|
166
|
-
<button type="button" title="Forget the source point" class={
|
|
167
|
+
<button type="button" title="Forget the source point" class={imageChipClass(false)} onclick={() => controller.setCloneSource(null)}>Clear source</button>
|
|
167
168
|
{/if}
|
|
168
169
|
</div>
|
|
169
170
|
<div class="flex flex-col gap-1" role="group" aria-label="Clone source transform" data-testid="clone-transform">
|
|
@@ -171,23 +172,23 @@
|
|
|
171
172
|
<ControlSlider {spec} value={view.cloneView[spec.id]} onchange={(v) => setCloneTransformValue(controller, spec.id, v)} />
|
|
172
173
|
{/each}
|
|
173
174
|
<div class="flex items-center gap-1">
|
|
174
|
-
<button type="button" aria-pressed={view.cloneView.flipH} title="Mirror the source left to right" class={
|
|
175
|
-
<button type="button" aria-pressed={view.cloneView.flipV} title="Mirror the source top to bottom" class={
|
|
175
|
+
<button type="button" aria-pressed={view.cloneView.flipH} title="Mirror the source left to right" class={imageChipClass(view.cloneView.flipH)} onclick={() => setCloneTransformValue(controller, 'flipH')}>Flip H</button>
|
|
176
|
+
<button type="button" aria-pressed={view.cloneView.flipV} title="Mirror the source top to bottom" class={imageChipClass(view.cloneView.flipV)} onclick={() => setCloneTransformValue(controller, 'flipV')}>Flip V</button>
|
|
176
177
|
{#if !view.cloneView.identity}
|
|
177
|
-
<button type="button" title="Back to 100 %, no flip, 0°" class={
|
|
178
|
+
<button type="button" title="Back to 100 %, no flip, 0°" class={imageChipClass(false)} onclick={() => controller.setCloneTransform(null)}>Reset transform</button>
|
|
178
179
|
{/if}
|
|
179
180
|
</div>
|
|
180
181
|
</div>
|
|
181
182
|
{/if}
|
|
182
183
|
{#each RETOUCH_CHOICES.filter((c) => c.tools.includes(view.tool as never)) as choice (choice.id)}
|
|
183
184
|
<div class="flex flex-wrap items-center gap-1" role="group" aria-label={choice.label}>
|
|
184
|
-
<span class="text-
|
|
185
|
+
<span class="text-muted-foreground">{choice.label}</span>
|
|
185
186
|
{#each choice.options as o (o.id)}
|
|
186
187
|
<button
|
|
187
188
|
type="button"
|
|
188
189
|
aria-pressed={view.retouch[choice.id] === o.id}
|
|
189
190
|
title={o.title}
|
|
190
|
-
class={
|
|
191
|
+
class={imageChipClass(view.retouch[choice.id] === o.id)}
|
|
191
192
|
onclick={() => setRetouchChoice(controller, choice.id, o.id)}>{o.label}</button>
|
|
192
193
|
{/each}
|
|
193
194
|
</div>
|
|
@@ -195,7 +196,7 @@
|
|
|
195
196
|
{#each RETOUCH_OPTION_CONTROLS.filter((c) => c.tools.includes(view.tool as never)) as spec (spec.id)}
|
|
196
197
|
<ControlSlider {spec} value={view.retouch[spec.id]} onchange={(v) => controller.setPaintOptions({ retouch: { [spec.id]: v } })} />
|
|
197
198
|
{/each}
|
|
198
|
-
<div class="flex flex-wrap gap-3 text-
|
|
199
|
+
<div class="flex flex-wrap gap-3 text-foreground">
|
|
199
200
|
{#each RETOUCH_TOGGLES.filter((t) => t.tools.includes(view.tool as never)) as t (t.id)}
|
|
200
201
|
<label class="flex items-center gap-1.5">
|
|
201
202
|
<input
|
|
@@ -203,7 +204,7 @@
|
|
|
203
204
|
aria-label={t.label}
|
|
204
205
|
checked={view.retouch[t.id]}
|
|
205
206
|
onchange={(e) => controller.setPaintOptions({ retouch: { [t.id]: (e.target as HTMLInputElement).checked } })}
|
|
206
|
-
class="accent-
|
|
207
|
+
class="accent-primary"
|
|
207
208
|
/>
|
|
208
209
|
{t.label}
|
|
209
210
|
</label>
|
|
@@ -212,14 +213,14 @@
|
|
|
212
213
|
</div>
|
|
213
214
|
{/if}
|
|
214
215
|
|
|
215
|
-
<div class="flex flex-wrap gap-3 text-
|
|
216
|
+
<div class="flex flex-wrap gap-3 text-foreground" role="group" aria-label="Pressure">
|
|
216
217
|
<label class="flex items-center gap-1.5">
|
|
217
218
|
<input
|
|
218
219
|
type="checkbox"
|
|
219
220
|
aria-label="Pressure controls size"
|
|
220
221
|
checked={view.brush.pressureSize}
|
|
221
222
|
onchange={(e) => controller.setPaintOptions({ brush: { pressureSize: (e.target as HTMLInputElement).checked } })}
|
|
222
|
-
class="accent-
|
|
223
|
+
class="accent-primary"
|
|
223
224
|
/>
|
|
224
225
|
Pressure size
|
|
225
226
|
</label>
|
|
@@ -229,17 +230,17 @@
|
|
|
229
230
|
aria-label="Pressure controls opacity"
|
|
230
231
|
checked={view.brush.pressureOpacity}
|
|
231
232
|
onchange={(e) => controller.setPaintOptions({ brush: { pressureOpacity: (e.target as HTMLInputElement).checked } })}
|
|
232
|
-
class="accent-
|
|
233
|
+
class="accent-primary"
|
|
233
234
|
/>
|
|
234
235
|
Pressure opacity
|
|
235
236
|
</label>
|
|
236
237
|
</div>
|
|
237
238
|
|
|
238
239
|
<div class="flex gap-1">
|
|
239
|
-
<button type="button" class={
|
|
240
|
-
<button type="button" class={
|
|
240
|
+
<button type="button" class={imageChipClass(false)} onclick={() => attempt(() => controller.fillPixels())}>Fill</button>
|
|
241
|
+
<button type="button" class={imageChipClass(false)} onclick={() => attempt(() => controller.clearPixels())}>Clear</button>
|
|
241
242
|
</div>
|
|
242
243
|
{#if error}
|
|
243
|
-
<p role="alert" class="text-
|
|
244
|
+
<p role="alert" class="text-destructive">{error}</p>
|
|
244
245
|
{/if}
|
|
245
246
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageChipClass } from './styles.js';
|
|
2
3
|
import type { ImageEditorController, Intent } from '@pixerate/editor/image-editor';
|
|
3
4
|
import { COLOR_MODE_OPTIONS, INTENT_OPTIONS, PROOF_PROFILE_OPTIONS, WORKING_SPACE_OPTIONS, colorPanelView } from '@pixerate/editor/image-editor';
|
|
4
5
|
import { untrack } from 'svelte';
|
|
@@ -24,9 +25,8 @@
|
|
|
24
25
|
let intent = $state<Intent>('relative');
|
|
25
26
|
let bpc = $state(true);
|
|
26
27
|
let error = $state<string | null>(null);
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const select = 'rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-200';
|
|
28
|
+
const chip = imageChipClass(false);
|
|
29
|
+
const select = 'rounded bg-muted px-1 py-0.5 text-[11px] text-foreground';
|
|
30
30
|
|
|
31
31
|
function attempt(fn: () => unknown) {
|
|
32
32
|
try {
|
|
@@ -51,20 +51,20 @@
|
|
|
51
51
|
}
|
|
52
52
|
</script>
|
|
53
53
|
|
|
54
|
-
<div class="flex flex-col gap-2 text-xs text-
|
|
54
|
+
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="color-profile-panel">
|
|
55
55
|
<div class="flex items-center gap-2">
|
|
56
|
-
<span class="text-
|
|
56
|
+
<span class="text-muted-foreground">Profile</span>
|
|
57
57
|
<span data-testid="color-profile-name">{view.profileName}</span>
|
|
58
58
|
</div>
|
|
59
59
|
<div class="flex flex-wrap items-center gap-1" role="group" aria-label="Colour mode">
|
|
60
|
-
<span class="text-
|
|
60
|
+
<span class="text-muted-foreground">Mode</span>
|
|
61
61
|
<select aria-label="Colour mode" class={select} value={view.mode} onchange={(e) => attempt(() => controller.setColorMode((e.target as HTMLSelectElement).value as 'rgb' | 'cmyk' | 'lab'))}>
|
|
62
62
|
{#each COLOR_MODE_OPTIONS as o (o.id)}
|
|
63
63
|
<option value={o.id} title={o.title}>{o.label}</option>
|
|
64
64
|
{/each}
|
|
65
65
|
</select>
|
|
66
66
|
{#if view.cmykProfileName}
|
|
67
|
-
<span class="text-
|
|
67
|
+
<span class="text-muted-foreground">{view.cmykProfileName}</span>
|
|
68
68
|
{/if}
|
|
69
69
|
</div>
|
|
70
70
|
<div class="flex flex-wrap items-center gap-1" role="group" aria-label="Assign or convert">
|
|
@@ -79,14 +79,14 @@
|
|
|
79
79
|
{/each}
|
|
80
80
|
</select>
|
|
81
81
|
<label class="flex items-center gap-1">
|
|
82
|
-
<input type="checkbox" aria-label="Black point compensation" checked={bpc} onchange={(e) => (bpc = (e.target as HTMLInputElement).checked)} class="accent-
|
|
82
|
+
<input type="checkbox" aria-label="Black point compensation" checked={bpc} onchange={(e) => (bpc = (e.target as HTMLInputElement).checked)} class="accent-primary" />
|
|
83
83
|
BPC
|
|
84
84
|
</label>
|
|
85
85
|
<button type="button" class={chip} title="Edit › Assign Profile: keep the numbers" onclick={() => attempt(() => controller.assignProfile(target))}>Assign</button>
|
|
86
86
|
<button type="button" class={chip} title="Edit › Convert to Profile: keep the look" onclick={() => attempt(() => controller.convertToProfile(target, { intent, bpc }))}>Convert</button>
|
|
87
87
|
</div>
|
|
88
88
|
<div class="flex flex-wrap items-center gap-1" role="group" aria-label="Proof setup">
|
|
89
|
-
<span class="text-
|
|
89
|
+
<span class="text-muted-foreground">Proof</span>
|
|
90
90
|
<select
|
|
91
91
|
aria-label="Proof profile"
|
|
92
92
|
class={select}
|
|
@@ -108,16 +108,16 @@
|
|
|
108
108
|
{/each}
|
|
109
109
|
</select>
|
|
110
110
|
<label class="flex items-center gap-1">
|
|
111
|
-
<input type="checkbox" aria-label="Simulate paper colour" checked={view.proof.simulatePaper} onchange={(e) => proofWith({ simulatePaper: (e.target as HTMLInputElement).checked })} class="accent-
|
|
111
|
+
<input type="checkbox" aria-label="Simulate paper colour" checked={view.proof.simulatePaper} onchange={(e) => proofWith({ simulatePaper: (e.target as HTMLInputElement).checked })} class="accent-primary" />
|
|
112
112
|
Paper
|
|
113
113
|
</label>
|
|
114
114
|
<label class="flex items-center gap-1">
|
|
115
|
-
<input type="checkbox" aria-label="Gamut warning" checked={view.proof.gamutWarning} onchange={(e) => proofWith({ gamutWarning: (e.target as HTMLInputElement).checked })} class="accent-
|
|
115
|
+
<input type="checkbox" aria-label="Gamut warning" checked={view.proof.gamutWarning} onchange={(e) => proofWith({ gamutWarning: (e.target as HTMLInputElement).checked })} class="accent-primary" />
|
|
116
116
|
Gamut warning
|
|
117
117
|
</label>
|
|
118
118
|
{/if}
|
|
119
119
|
</div>
|
|
120
120
|
{#if error}
|
|
121
|
-
<p role="alert" class="text-
|
|
121
|
+
<p role="alert" class="text-destructive">{error}</p>
|
|
122
122
|
{/if}
|
|
123
123
|
</div>
|
|
@@ -9,16 +9,18 @@
|
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
/** Overrides `spec.label` (also the accessible name). */
|
|
11
11
|
label?: string;
|
|
12
|
+
/** Extra classes on the root element. */
|
|
13
|
+
class?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
let { spec, value, onchange, disabled = false, label }: Props = $props();
|
|
16
|
+
let { spec, value, onchange, disabled = false, label, class: className = '' }: Props = $props();
|
|
15
17
|
|
|
16
18
|
const name = $derived(label ?? spec.label);
|
|
17
19
|
const track = $derived(controlTrackBackground(spec));
|
|
18
20
|
</script>
|
|
19
21
|
|
|
20
|
-
<div data-control-id={spec.id}>
|
|
21
|
-
<div class="flex justify-between text-
|
|
22
|
+
<div data-control-id={spec.id} class={className || undefined}>
|
|
23
|
+
<div class="flex justify-between text-muted-foreground mb-1">
|
|
22
24
|
<span>{name}</span>
|
|
23
25
|
<span class="font-mono">{formatControlValue(spec, value)}</span>
|
|
24
26
|
</div>
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
{disabled}
|
|
32
34
|
{value}
|
|
33
35
|
oninput={(e) => onchange(Number((e.target as HTMLInputElement).value))}
|
|
34
|
-
class="w-full accent-
|
|
36
|
+
class="w-full accent-primary disabled:opacity-40"
|
|
35
37
|
/>
|
|
36
38
|
{#if track}
|
|
37
39
|
<div class="h-0.5 rounded-full opacity-70" style="background: {track}"></div>
|
|
@@ -6,6 +6,8 @@ interface Props {
|
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
/** Overrides `spec.label` (also the accessible name). */
|
|
8
8
|
label?: string;
|
|
9
|
+
/** Extra classes on the root element. */
|
|
10
|
+
class?: string;
|
|
9
11
|
}
|
|
10
12
|
declare const ControlSlider: import("svelte").Component<Props, {}, "">;
|
|
11
13
|
type ControlSlider = ReturnType<typeof ControlSlider>;
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
histogram?: ImageHistogram | null;
|
|
19
19
|
channel?: 'master' | 'red' | 'green' | 'blue';
|
|
20
20
|
size?: number;
|
|
21
|
+
/** Extra classes on the root element. */
|
|
22
|
+
class?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
let { points, onchange, histogram = null, channel = 'master', size = 200 }: Props = $props();
|
|
25
|
+
let { points, onchange, histogram = null, channel = 'master', size = 200, class: className = '' }: Props = $props();
|
|
24
26
|
|
|
25
27
|
const BOX = 256;
|
|
26
|
-
const STROKE = { master: '#e2e8f0', red: '#ef4444', green: '#22c55e', blue: '#3b82f6' };
|
|
28
|
+
const STROKE = { master: 'var(--foreground, #e2e8f0)', red: '#ef4444', green: '#22c55e', blue: '#3b82f6' };
|
|
27
29
|
|
|
28
30
|
let svgEl: SVGSVGElement | undefined = $state();
|
|
29
31
|
let drag: number | null = $state(null);
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
role="application"
|
|
73
75
|
aria-label="{channel} curve"
|
|
74
76
|
data-testid="curves-editor"
|
|
75
|
-
class="rounded bg-
|
|
77
|
+
class="rounded bg-muted/40 touch-none select-none cursor-crosshair {className}"
|
|
76
78
|
{onpointerdown}
|
|
77
79
|
{onpointermove}
|
|
78
80
|
onpointerup={() => (drag = null)}
|
|
@@ -96,7 +98,7 @@
|
|
|
96
98
|
cx={b.x}
|
|
97
99
|
cy={b.y}
|
|
98
100
|
r="5"
|
|
99
|
-
fill={drag === i ? STROKE[channel] : '#0f172a'}
|
|
101
|
+
fill={drag === i ? STROKE[channel] : 'var(--background, #0f172a)'}
|
|
100
102
|
stroke={STROKE[channel]}
|
|
101
103
|
stroke-width="2"
|
|
102
104
|
aria-label="Point {p.input} to {p.output}"
|