@pixerate/editor-svelte 0.22.0 → 0.23.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/README.md +4 -0
- package/dist/MergePreview.svelte +201 -0
- package/dist/MergePreview.svelte.d.ts +25 -0
- package/dist/MergePreview.svelte.d.ts.map +1 -0
- package/dist/TemplateRenderer.svelte +4 -10
- package/dist/TemplateRenderer.svelte.d.ts.map +1 -1
- package/dist/canvas/createCanvasArrange.svelte.d.ts.map +1 -1
- package/dist/canvas/createCanvasAutoLayout.svelte.d.ts +68 -0
- package/dist/canvas/createCanvasAutoLayout.svelte.d.ts.map +1 -0
- package/dist/canvas/createCanvasAutoLayout.svelte.js +78 -0
- package/dist/canvas/index.d.ts +2 -0
- package/dist/canvas/index.d.ts.map +1 -1
- package/dist/canvas/index.js +3 -0
- package/dist/find-replace/FindReplaceBar.svelte +203 -0
- package/dist/find-replace/FindReplaceBar.svelte.d.ts +15 -0
- package/dist/find-replace/FindReplaceBar.svelte.d.ts.map +1 -0
- package/dist/find-replace/findReplace.svelte.d.ts +27 -0
- package/dist/find-replace/findReplace.svelte.d.ts.map +1 -0
- package/dist/find-replace/findReplace.svelte.js +79 -0
- package/dist/find-replace/index.d.ts +6 -0
- package/dist/find-replace/index.d.ts.map +1 -0
- package/dist/find-replace/index.js +2 -0
- package/dist/generation/GenerationSettings.svelte +174 -0
- package/dist/generation/GenerationSettings.svelte.d.ts +23 -0
- package/dist/generation/GenerationSettings.svelte.d.ts.map +1 -0
- package/dist/generation/JobProgress.svelte +53 -0
- package/dist/generation/JobProgress.svelte.d.ts +17 -0
- package/dist/generation/JobProgress.svelte.d.ts.map +1 -0
- package/dist/generation/index.d.ts +10 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +4 -0
- package/dist/generation/plan.svelte.d.ts +16 -0
- package/dist/generation/plan.svelte.d.ts.map +1 -0
- package/dist/generation/plan.svelte.js +26 -0
- package/dist/generation/progress.svelte.d.ts +25 -0
- package/dist/generation/progress.svelte.d.ts.map +1 -0
- package/dist/generation/progress.svelte.js +36 -0
- package/dist/image-editor/AdjustmentLayersPanel.svelte +309 -0
- package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +11 -0
- package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts.map +1 -0
- package/dist/image-editor/ControlSlider.svelte +39 -0
- package/dist/image-editor/ControlSlider.svelte.d.ts +13 -0
- package/dist/image-editor/ControlSlider.svelte.d.ts.map +1 -0
- package/dist/image-editor/CurvesEditor.svelte +105 -0
- package/dist/image-editor/CurvesEditor.svelte.d.ts +13 -0
- package/dist/image-editor/CurvesEditor.svelte.d.ts.map +1 -0
- package/dist/image-editor/Histogram.svelte +49 -0
- package/dist/image-editor/Histogram.svelte.d.ts +12 -0
- package/dist/image-editor/Histogram.svelte.d.ts.map +1 -0
- package/dist/image-editor/ImageEditor.svelte +32 -140
- package/dist/image-editor/ImageEditor.svelte.d.ts.map +1 -1
- package/dist/image-editor/imageEditorState.svelte.d.ts +14 -1
- package/dist/image-editor/imageEditorState.svelte.d.ts.map +1 -1
- package/dist/image-editor/imageEditorState.svelte.js +40 -0
- package/dist/image-editor/index.d.ts +12 -0
- package/dist/image-editor/index.d.ts.map +1 -1
- package/dist/image-editor/index.js +4 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/merge.svelte.d.ts +51 -0
- package/dist/merge.svelte.d.ts.map +1 -0
- package/dist/merge.svelte.js +117 -0
- package/dist/persistence/RecoveryPrompt.svelte +57 -0
- package/dist/persistence/RecoveryPrompt.svelte.d.ts +18 -0
- package/dist/persistence/RecoveryPrompt.svelte.d.ts.map +1 -0
- package/dist/persistence/autosave.svelte.d.ts +63 -0
- package/dist/persistence/autosave.svelte.d.ts.map +1 -0
- package/dist/persistence/autosave.svelte.js +110 -0
- package/dist/persistence/index.d.ts +6 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +2 -0
- package/dist/review/documentCompare.svelte.d.ts +21 -0
- package/dist/review/documentCompare.svelte.d.ts.map +1 -0
- package/dist/review/documentCompare.svelte.js +23 -0
- package/dist/review/index.d.ts +1 -0
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +1 -0
- package/dist/spreadsheet/SpreadsheetContextMenu.svelte +6 -3
- package/dist/spreadsheet/SpreadsheetContextMenu.svelte.d.ts.map +1 -1
- package/dist/spreadsheet/SpreadsheetEditor.svelte +6 -2
- package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts.map +1 -1
- package/dist/spreadsheet/spreadsheet.svelte.d.ts +2 -0
- package/dist/spreadsheet/spreadsheet.svelte.d.ts.map +1 -1
- package/package.json +18 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.svelte.d.ts","sourceRoot":"","sources":["../../src/generation/plan.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,4BAA4B;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,cAAc,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,iBAAiB,EAChC,YAAY,EAAE,MAAM,iBAAiB,EACrC,OAAO,GAAE,MAAM,WAAwB,GACtC,oBAAoB,CAmBtB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { planGenerationRequest, } from "@pixerate/editor/generation";
|
|
2
|
+
/**
|
|
3
|
+
* Reactive `planGenerationRequest` (Svelte 5 runes). Pass getters so the plan
|
|
4
|
+
* follows `$state` request/capabilities/options:
|
|
5
|
+
* `createGenerationPlan(() => request, () => caps, () => ({ strategy }))`.
|
|
6
|
+
*/
|
|
7
|
+
export function createGenerationPlan(request, capabilities, options = () => ({})) {
|
|
8
|
+
const plan = $derived(planGenerationRequest(request(), capabilities(), options()));
|
|
9
|
+
return {
|
|
10
|
+
get plan() {
|
|
11
|
+
return plan;
|
|
12
|
+
},
|
|
13
|
+
get ok() {
|
|
14
|
+
return plan.ok;
|
|
15
|
+
},
|
|
16
|
+
get request() {
|
|
17
|
+
return plan.request;
|
|
18
|
+
},
|
|
19
|
+
get adjustments() {
|
|
20
|
+
return plan.adjustments;
|
|
21
|
+
},
|
|
22
|
+
get errors() {
|
|
23
|
+
return plan.errors;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type JobProgressEstimate } from "@pixerate/editor/generation";
|
|
2
|
+
export interface JobProgressOptions {
|
|
3
|
+
startedAt: number | Date | string;
|
|
4
|
+
expectedMs?: number;
|
|
5
|
+
done?: boolean;
|
|
6
|
+
/** Fixed current time (disables ticking; useful for SSR and tests). */
|
|
7
|
+
now?: number | Date | string;
|
|
8
|
+
/** Update interval while running in ms (default 1000). */
|
|
9
|
+
intervalMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SvelteJobProgress {
|
|
12
|
+
readonly progress: JobProgressEstimate;
|
|
13
|
+
readonly percent: number;
|
|
14
|
+
readonly label: string;
|
|
15
|
+
/** Stops ticking. */
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Ticking `estimateJobProgress` (Svelte 5 runes). Pass a getter so it follows
|
|
20
|
+
* `$state` options; it ticks every `intervalMs` while not `done` and no fixed
|
|
21
|
+
* `now` is given (browser only; nothing ticks during SSR). Call `destroy()`
|
|
22
|
+
* when done (e.g. in `onDestroy`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function createJobProgress(options: () => JobProgressOptions): SvelteJobProgress;
|
|
25
|
+
//# sourceMappingURL=progress.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.svelte.d.ts","sourceRoot":"","sources":["../../src/generation/progress.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAE5F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IAC7B,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qBAAqB;IACrB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,kBAAkB,GAAG,iBAAiB,CA2BtF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { estimateJobProgress } from "@pixerate/editor/generation";
|
|
2
|
+
/**
|
|
3
|
+
* Ticking `estimateJobProgress` (Svelte 5 runes). Pass a getter so it follows
|
|
4
|
+
* `$state` options; it ticks every `intervalMs` while not `done` and no fixed
|
|
5
|
+
* `now` is given (browser only; nothing ticks during SSR). Call `destroy()`
|
|
6
|
+
* when done (e.g. in `onDestroy`).
|
|
7
|
+
*/
|
|
8
|
+
export function createJobProgress(options) {
|
|
9
|
+
let tick = $state(Date.now());
|
|
10
|
+
const timer = typeof window !== "undefined"
|
|
11
|
+
? setInterval(() => {
|
|
12
|
+
const current = options();
|
|
13
|
+
if (!current.done && current.now === undefined)
|
|
14
|
+
tick = Date.now();
|
|
15
|
+
}, Math.max(16, options().intervalMs ?? 1000))
|
|
16
|
+
: null;
|
|
17
|
+
const progress = $derived.by(() => {
|
|
18
|
+
const { startedAt, expectedMs, done, now } = options();
|
|
19
|
+
return estimateJobProgress({ startedAt, expectedMs, done, now: now ?? tick });
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
get progress() {
|
|
23
|
+
return progress;
|
|
24
|
+
},
|
|
25
|
+
get percent() {
|
|
26
|
+
return progress.percent;
|
|
27
|
+
},
|
|
28
|
+
get label() {
|
|
29
|
+
return progress.label;
|
|
30
|
+
},
|
|
31
|
+
destroy() {
|
|
32
|
+
if (timer !== null)
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
AdjustmentKind,
|
|
4
|
+
AdjustmentLayer,
|
|
5
|
+
BlendMode,
|
|
6
|
+
ControlSpec,
|
|
7
|
+
CurvePoint,
|
|
8
|
+
ImageEditorController,
|
|
9
|
+
ImageHistogram,
|
|
10
|
+
} from '@pixerate/editor/image-editor';
|
|
11
|
+
import {
|
|
12
|
+
ADJUSTMENT_KINDS,
|
|
13
|
+
ADJUSTMENT_LAYER_CONTROLS,
|
|
14
|
+
ADJUSTMENT_LAYER_OPACITY_CONTROL,
|
|
15
|
+
BLEND_MODES,
|
|
16
|
+
HUE_RANGE_LABELS,
|
|
17
|
+
adjustmentKindLabel,
|
|
18
|
+
getControlPath,
|
|
19
|
+
hueRangeWeight,
|
|
20
|
+
setControlPath,
|
|
21
|
+
} from '@pixerate/editor/image-editor';
|
|
22
|
+
import { untrack } from 'svelte';
|
|
23
|
+
import ControlSlider from './ControlSlider.svelte';
|
|
24
|
+
import CurvesEditor from './CurvesEditor.svelte';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
controller: ImageEditorController;
|
|
28
|
+
/** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
|
|
29
|
+
layers?: AdjustmentLayer[];
|
|
30
|
+
histogram?: ImageHistogram | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let { controller, layers: layersProp, histogram = null }: Props = $props();
|
|
34
|
+
|
|
35
|
+
// Follows the controller when no `layers` prop is given.
|
|
36
|
+
let tick = $state(0);
|
|
37
|
+
$effect(() => {
|
|
38
|
+
const c = controller;
|
|
39
|
+
// `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
|
|
40
|
+
return untrack(() => c.subscribe(() => tick++));
|
|
41
|
+
});
|
|
42
|
+
const layers = $derived.by(() => {
|
|
43
|
+
void tick;
|
|
44
|
+
return layersProp ?? controller.getAdjustmentLayers();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type Channel = 'master' | 'red' | 'green' | 'blue';
|
|
48
|
+
const CHANNELS: { id: Channel; label: string }[] = [
|
|
49
|
+
{ id: 'master', label: 'RGB' },
|
|
50
|
+
{ id: 'red', label: 'Red' },
|
|
51
|
+
{ id: 'green', label: 'Green' },
|
|
52
|
+
{ id: 'blue', label: 'Blue' },
|
|
53
|
+
];
|
|
54
|
+
const TONES = [
|
|
55
|
+
{ id: 'shadows', label: 'Shadows' },
|
|
56
|
+
{ id: 'midtones', label: 'Midtones' },
|
|
57
|
+
{ id: 'highlights', label: 'Highlights' },
|
|
58
|
+
] as const;
|
|
59
|
+
type Tone = (typeof TONES)[number]['id'];
|
|
60
|
+
|
|
61
|
+
let newKind: AdjustmentKind = $state('curves');
|
|
62
|
+
let selectedId: string | null = $state(null);
|
|
63
|
+
let channel: Channel = $state('master');
|
|
64
|
+
let range = $state(-1);
|
|
65
|
+
let tone: Tone = $state('midtones');
|
|
66
|
+
|
|
67
|
+
const selected = $derived(layers.find((l) => l.id === selectedId) ?? null);
|
|
68
|
+
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
|
+
const chipClass = (on: boolean) =>
|
|
72
|
+
`px-2 py-0.5 rounded text-[10px] ${on ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-400 hover:text-slate-200'}`;
|
|
73
|
+
|
|
74
|
+
function setParam(layer: AdjustmentLayer, path: string, value: unknown, commit = true) {
|
|
75
|
+
controller.updateAdjustmentLayer(layer.id, { params: setControlPath(layer.params, path, value) as never }, commit);
|
|
76
|
+
}
|
|
77
|
+
const paramValue = (layer: AdjustmentLayer, spec: ControlSpec) => Number(getControlPath(layer.params, spec.id) ?? spec.default);
|
|
78
|
+
|
|
79
|
+
function visibleControls(layer: AdjustmentLayer): ControlSpec[] {
|
|
80
|
+
const all = ADJUSTMENT_LAYER_CONTROLS[layer.kind];
|
|
81
|
+
switch (layer.kind) {
|
|
82
|
+
case 'levels':
|
|
83
|
+
return all.filter((s) => s.id.startsWith(`${channel}.`));
|
|
84
|
+
case 'hueSaturation':
|
|
85
|
+
return all.filter((s) =>
|
|
86
|
+
range === -1 || layer.params.colorize ? !s.id.startsWith('ranges.') : s.id.startsWith(`ranges.${range}.`)
|
|
87
|
+
);
|
|
88
|
+
case 'colorBalance':
|
|
89
|
+
return all.filter((s) => s.id.startsWith(`${tone}.`));
|
|
90
|
+
default:
|
|
91
|
+
return [...all];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stripPoints(bounds: [number, number, number, number]) {
|
|
96
|
+
return Array.from({ length: 73 }, (_, i) => `${(i / 72) * 360},${20 - hueRangeWeight({ bounds }, i * 5) * 18}`).join(' ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function addLayer() {
|
|
100
|
+
selectedId = controller.addAdjustmentLayer(newKind);
|
|
101
|
+
channel = 'master';
|
|
102
|
+
range = -1;
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<div class="flex flex-col gap-2 text-xs" data-testid="adjustment-layers-panel">
|
|
107
|
+
<div class="flex items-center justify-between">
|
|
108
|
+
<span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Adjustment Layers</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="flex gap-1.5">
|
|
111
|
+
<select aria-label="New adjustment layer kind" class="{selectClass} flex-1" bind:value={newKind}>
|
|
112
|
+
{#each ADJUSTMENT_KINDS as k (k.kind)}
|
|
113
|
+
<option value={k.kind}>{k.label}</option>
|
|
114
|
+
{/each}
|
|
115
|
+
</select>
|
|
116
|
+
<button type="button" class="px-2 py-1 rounded bg-indigo-600 text-white hover:bg-indigo-500" onclick={addLayer}>
|
|
117
|
+
Add Layer
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{#if topFirst.length > 0}
|
|
122
|
+
<ul class="flex flex-col gap-1" aria-label="Adjustment layers">
|
|
123
|
+
{#each topFirst as layer (layer.id)}
|
|
124
|
+
{@const index = layers.indexOf(layer)}
|
|
125
|
+
{@const label = layer.name || adjustmentKindLabel(layer.kind)}
|
|
126
|
+
<li
|
|
127
|
+
class="flex items-center gap-1.5 rounded px-1.5 py-1 {layer.id === selectedId
|
|
128
|
+
? 'bg-indigo-600/20 ring-1 ring-indigo-500/50'
|
|
129
|
+
: 'bg-slate-800/60'}"
|
|
130
|
+
>
|
|
131
|
+
<input
|
|
132
|
+
type="checkbox"
|
|
133
|
+
aria-label="Show {label}"
|
|
134
|
+
checked={layer.enabled}
|
|
135
|
+
onchange={() => controller.toggleAdjustmentLayer(layer.id)}
|
|
136
|
+
class="accent-indigo-500"
|
|
137
|
+
/>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
class="flex-1 text-left truncate {layer.enabled ? 'text-slate-200' : 'text-slate-500 line-through'}"
|
|
141
|
+
aria-pressed={layer.id === selectedId}
|
|
142
|
+
onclick={() => (selectedId = layer.id === selectedId ? null : layer.id)}
|
|
143
|
+
>
|
|
144
|
+
{label}
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
aria-label="Move {label} up"
|
|
149
|
+
disabled={index === layers.length - 1}
|
|
150
|
+
onclick={() => controller.moveAdjustmentLayer(layer.id, index + 1)}
|
|
151
|
+
class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
|
|
152
|
+
>
|
|
153
|
+
↑
|
|
154
|
+
</button>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
aria-label="Move {label} down"
|
|
158
|
+
disabled={index === 0}
|
|
159
|
+
onclick={() => controller.moveAdjustmentLayer(layer.id, index - 1)}
|
|
160
|
+
class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
|
|
161
|
+
>
|
|
162
|
+
↓
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
aria-label="Remove {label}"
|
|
167
|
+
onclick={() => {
|
|
168
|
+
controller.removeAdjustmentLayer(layer.id);
|
|
169
|
+
if (selectedId === layer.id) selectedId = null;
|
|
170
|
+
}}
|
|
171
|
+
class="px-1 text-rose-400 hover:text-rose-300"
|
|
172
|
+
>
|
|
173
|
+
✕
|
|
174
|
+
</button>
|
|
175
|
+
</li>
|
|
176
|
+
{/each}
|
|
177
|
+
</ul>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
{#if selected}
|
|
181
|
+
{@const layer = selected}
|
|
182
|
+
<div class="flex flex-col gap-2 rounded border border-slate-700/60 p-2" data-testid="adjustment-layer-editor">
|
|
183
|
+
<div class="flex items-center gap-2">
|
|
184
|
+
<span class="text-slate-400">Blend</span>
|
|
185
|
+
<select
|
|
186
|
+
aria-label="Blend mode"
|
|
187
|
+
class="{selectClass} flex-1"
|
|
188
|
+
value={layer.blend}
|
|
189
|
+
onchange={(e) => controller.updateAdjustmentLayer(layer.id, { blend: (e.target as HTMLSelectElement).value as BlendMode })}
|
|
190
|
+
>
|
|
191
|
+
{#each BLEND_MODES as m (m.id)}
|
|
192
|
+
<option value={m.id}>{m.label}</option>
|
|
193
|
+
{/each}
|
|
194
|
+
</select>
|
|
195
|
+
</div>
|
|
196
|
+
<ControlSlider
|
|
197
|
+
spec={ADJUSTMENT_LAYER_OPACITY_CONTROL}
|
|
198
|
+
label="Layer opacity"
|
|
199
|
+
value={layer.opacity}
|
|
200
|
+
onchange={(v) => controller.updateAdjustmentLayer(layer.id, { opacity: v })}
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
{#if layer.kind === 'curves' || layer.kind === 'levels'}
|
|
204
|
+
<div class="flex gap-1" role="group" aria-label={layer.kind === 'curves' ? 'Curve channel' : 'Levels channel'}>
|
|
205
|
+
{#each CHANNELS as c (c.id)}
|
|
206
|
+
<button type="button" class={chipClass(channel === c.id)} onclick={() => (channel = c.id)}>{c.label}</button>
|
|
207
|
+
{/each}
|
|
208
|
+
</div>
|
|
209
|
+
{/if}
|
|
210
|
+
|
|
211
|
+
{#if layer.kind === 'curves'}
|
|
212
|
+
<CurvesEditor
|
|
213
|
+
points={layer.params[channel]}
|
|
214
|
+
{channel}
|
|
215
|
+
{histogram}
|
|
216
|
+
onchange={(points: CurvePoint[], commit: boolean) => setParam(layer, channel, points, commit)}
|
|
217
|
+
/>
|
|
218
|
+
{:else if layer.kind === 'hueSaturation'}
|
|
219
|
+
<div class="flex flex-wrap gap-1" role="group" aria-label="Hue range">
|
|
220
|
+
<button type="button" class={chipClass(range === -1)} onclick={() => (range = -1)}>Master</button>
|
|
221
|
+
{#each HUE_RANGE_LABELS as name, i (name)}
|
|
222
|
+
<button type="button" class={chipClass(range === i)} disabled={layer.params.colorize} onclick={() => (range = i)}>
|
|
223
|
+
{name}
|
|
224
|
+
</button>
|
|
225
|
+
{/each}
|
|
226
|
+
</div>
|
|
227
|
+
{#if range >= 0 && !layer.params.colorize}
|
|
228
|
+
<svg viewBox="0 0 360 20" preserveAspectRatio="none" class="w-full h-4 rounded" data-testid="hue-range-strip">
|
|
229
|
+
<defs>
|
|
230
|
+
<linearGradient id="pixerate-hue-strip-svelte">
|
|
231
|
+
{#each [0, 60, 120, 180, 240, 300, 360] as h (h)}
|
|
232
|
+
<stop offset="{(h / 360) * 100}%" stop-color="hsl({h}, 90%, 50%)" />
|
|
233
|
+
{/each}
|
|
234
|
+
</linearGradient>
|
|
235
|
+
</defs>
|
|
236
|
+
<rect width="360" height="20" fill="url(#pixerate-hue-strip-svelte)" opacity="0.6" />
|
|
237
|
+
<polyline points={stripPoints(layer.params.ranges[range].bounds)} fill="none" stroke="#fff" stroke-width="1.5" />
|
|
238
|
+
</svg>
|
|
239
|
+
{/if}
|
|
240
|
+
{:else if layer.kind === 'colorBalance'}
|
|
241
|
+
<div class="flex gap-1" role="group" aria-label="Tone">
|
|
242
|
+
{#each TONES as t (t.id)}
|
|
243
|
+
<button type="button" class={chipClass(tone === t.id)} onclick={() => (tone = t.id)}>{t.label}</button>
|
|
244
|
+
{/each}
|
|
245
|
+
</div>
|
|
246
|
+
{:else if layer.kind === 'photoFilter'}
|
|
247
|
+
<label class="flex items-center gap-2 text-slate-300">
|
|
248
|
+
Filter colour
|
|
249
|
+
<input
|
|
250
|
+
type="color"
|
|
251
|
+
aria-label="Filter colour"
|
|
252
|
+
value={layer.params.color}
|
|
253
|
+
oninput={(e) => setParam(layer, 'color', (e.target as HTMLInputElement).value)}
|
|
254
|
+
class="h-5 w-8 bg-transparent"
|
|
255
|
+
/>
|
|
256
|
+
</label>
|
|
257
|
+
{:else if layer.kind === 'invert'}
|
|
258
|
+
<p class="text-slate-500 text-[11px]">Invert has no settings.</p>
|
|
259
|
+
{/if}
|
|
260
|
+
|
|
261
|
+
{#each visibleControls(layer) as spec (spec.id)}
|
|
262
|
+
<ControlSlider {spec} value={paramValue(layer, spec)} onchange={(v) => setParam(layer, spec.id, v)} />
|
|
263
|
+
{/each}
|
|
264
|
+
|
|
265
|
+
{#if layer.kind === 'hueSaturation' && range === -1}
|
|
266
|
+
<label class="flex items-center gap-2 text-slate-300">
|
|
267
|
+
<input
|
|
268
|
+
type="checkbox"
|
|
269
|
+
checked={layer.params.colorize}
|
|
270
|
+
onchange={(e) => setParam(layer, 'colorize', (e.target as HTMLInputElement).checked)}
|
|
271
|
+
class="accent-indigo-500"
|
|
272
|
+
/>
|
|
273
|
+
Colorize
|
|
274
|
+
</label>
|
|
275
|
+
{/if}
|
|
276
|
+
{#if layer.kind === 'colorBalance' || layer.kind === 'photoFilter'}
|
|
277
|
+
<label class="flex items-center gap-2 text-slate-300">
|
|
278
|
+
<input
|
|
279
|
+
type="checkbox"
|
|
280
|
+
checked={layer.params.preserveLuminosity}
|
|
281
|
+
onchange={(e) => setParam(layer, 'preserveLuminosity', (e.target as HTMLInputElement).checked)}
|
|
282
|
+
class="accent-indigo-500"
|
|
283
|
+
/>
|
|
284
|
+
Preserve luminosity
|
|
285
|
+
</label>
|
|
286
|
+
{/if}
|
|
287
|
+
{#if layer.kind === 'blackWhite'}
|
|
288
|
+
<label class="flex items-center gap-2 text-slate-300">
|
|
289
|
+
<input
|
|
290
|
+
type="checkbox"
|
|
291
|
+
checked={layer.params.tint !== null}
|
|
292
|
+
onchange={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).checked ? '#e1d3b3' : null)}
|
|
293
|
+
class="accent-indigo-500"
|
|
294
|
+
/>
|
|
295
|
+
Tint
|
|
296
|
+
{#if layer.params.tint}
|
|
297
|
+
<input
|
|
298
|
+
type="color"
|
|
299
|
+
aria-label="Tint colour"
|
|
300
|
+
value={layer.params.tint}
|
|
301
|
+
oninput={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).value)}
|
|
302
|
+
class="h-5 w-8 bg-transparent"
|
|
303
|
+
/>
|
|
304
|
+
{/if}
|
|
305
|
+
</label>
|
|
306
|
+
{/if}
|
|
307
|
+
</div>
|
|
308
|
+
{/if}
|
|
309
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AdjustmentLayer, ImageEditorController, ImageHistogram } from '@pixerate/editor/image-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: ImageEditorController;
|
|
4
|
+
/** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
|
|
5
|
+
layers?: AdjustmentLayer[];
|
|
6
|
+
histogram?: ImageHistogram | null;
|
|
7
|
+
}
|
|
8
|
+
declare const AdjustmentLayersPanel: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type AdjustmentLayersPanel = ReturnType<typeof AdjustmentLayersPanel>;
|
|
10
|
+
export default AdjustmentLayersPanel;
|
|
11
|
+
//# sourceMappingURL=AdjustmentLayersPanel.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdjustmentLayersPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/AdjustmentLayersPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAER,eAAe,EAIf,qBAAqB,EACrB,cAAc,EACf,MAAM,+BAA+B,CAAC;AAiBvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,qGAAqG;IACrG,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AA0NH,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ControlSpec } from '@pixerate/editor/image-editor';
|
|
3
|
+
import { controlTrackBackground, formatControlValue } from '@pixerate/editor/image-editor';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
spec: ControlSpec;
|
|
7
|
+
value: number;
|
|
8
|
+
onchange: (value: number) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Overrides `spec.label` (also the accessible name). */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { spec, value, onchange, disabled = false, label }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const name = $derived(label ?? spec.label);
|
|
17
|
+
const track = $derived(controlTrackBackground(spec));
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div data-control-id={spec.id}>
|
|
21
|
+
<div class="flex justify-between text-slate-400 mb-1">
|
|
22
|
+
<span>{name}</span>
|
|
23
|
+
<span class="font-mono">{formatControlValue(spec, value)}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<input
|
|
26
|
+
type="range"
|
|
27
|
+
min={spec.min}
|
|
28
|
+
max={spec.max}
|
|
29
|
+
step={spec.step}
|
|
30
|
+
aria-label={name}
|
|
31
|
+
{disabled}
|
|
32
|
+
{value}
|
|
33
|
+
oninput={(e) => onchange(Number((e.target as HTMLInputElement).value))}
|
|
34
|
+
class="w-full accent-indigo-500 disabled:opacity-40"
|
|
35
|
+
/>
|
|
36
|
+
{#if track}
|
|
37
|
+
<div class="h-0.5 rounded-full opacity-70" style="background: {track}"></div>
|
|
38
|
+
{/if}
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ControlSpec } from '@pixerate/editor/image-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
spec: ControlSpec;
|
|
4
|
+
value: number;
|
|
5
|
+
onchange: (value: number) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
/** Overrides `spec.label` (also the accessible name). */
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const ControlSlider: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type ControlSlider = ReturnType<typeof ControlSlider>;
|
|
12
|
+
export default ControlSlider;
|
|
13
|
+
//# sourceMappingURL=ControlSlider.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ControlSlider.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/ControlSlider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAI/D,UAAU,KAAK;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyBH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CurvePoint, ImageHistogram } from '@pixerate/editor/image-editor';
|
|
3
|
+
import {
|
|
4
|
+
boxToCurvePoint,
|
|
5
|
+
curvePointToBox,
|
|
6
|
+
curveSvgPath,
|
|
7
|
+
hitTestCurvePoint,
|
|
8
|
+
histogramPath,
|
|
9
|
+
insertCurvePoint,
|
|
10
|
+
moveCurvePoint,
|
|
11
|
+
removeCurvePoint,
|
|
12
|
+
} from '@pixerate/editor/image-editor';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
points: CurvePoint[];
|
|
16
|
+
/** `commit` is true for the first change of a gesture (one undo step), false for the drag moves. */
|
|
17
|
+
onchange: (points: CurvePoint[], commit: boolean) => void;
|
|
18
|
+
histogram?: ImageHistogram | null;
|
|
19
|
+
channel?: 'master' | 'red' | 'green' | 'blue';
|
|
20
|
+
size?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { points, onchange, histogram = null, channel = 'master', size = 200 }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const BOX = 256;
|
|
26
|
+
const STROKE = { master: '#e2e8f0', red: '#ef4444', green: '#22c55e', blue: '#3b82f6' };
|
|
27
|
+
|
|
28
|
+
let svgEl: SVGSVGElement | undefined = $state();
|
|
29
|
+
let drag: number | null = $state(null);
|
|
30
|
+
|
|
31
|
+
function toBox(e: MouseEvent) {
|
|
32
|
+
const rect = svgEl?.getBoundingClientRect();
|
|
33
|
+
if (!rect || !rect.width || !rect.height) return { x: 0, y: 0 };
|
|
34
|
+
return { x: ((e.clientX - rect.left) / rect.width) * BOX, y: ((e.clientY - rect.top) / rect.height) * BOX };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function onpointerdown(e: PointerEvent) {
|
|
38
|
+
const { x, y } = toBox(e);
|
|
39
|
+
let index = hitTestCurvePoint(points, x, y, BOX, BOX, 10);
|
|
40
|
+
if (index === -1) {
|
|
41
|
+
const inserted = insertCurvePoint(points, boxToCurvePoint(x, y, BOX, BOX));
|
|
42
|
+
onchange(inserted.points, true);
|
|
43
|
+
index = inserted.index;
|
|
44
|
+
} else {
|
|
45
|
+
onchange(points, true);
|
|
46
|
+
}
|
|
47
|
+
drag = index;
|
|
48
|
+
(e.target as Element).setPointerCapture?.(e.pointerId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onpointermove(e: PointerEvent) {
|
|
52
|
+
if (drag === null) return;
|
|
53
|
+
const { x, y } = toBox(e);
|
|
54
|
+
onchange(moveCurvePoint(points, drag, boxToCurvePoint(x, y, BOX, BOX)), false);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ondblclick(e: MouseEvent) {
|
|
58
|
+
const { x, y } = toBox(e);
|
|
59
|
+
const index = hitTestCurvePoint(points, x, y, BOX, BOX, 10);
|
|
60
|
+
if (index !== -1 && points.length > 2) onchange(removeCurvePoint(points, index), true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const histChannel = $derived(channel === 'master' ? 'luminance' : channel);
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<!-- Curves editor over the histogram: click to add a point, drag to move it, double-click to remove it. -->
|
|
67
|
+
<svg
|
|
68
|
+
bind:this={svgEl}
|
|
69
|
+
viewBox="0 0 {BOX} {BOX}"
|
|
70
|
+
width={size}
|
|
71
|
+
height={size}
|
|
72
|
+
role="application"
|
|
73
|
+
aria-label="{channel} curve"
|
|
74
|
+
data-testid="curves-editor"
|
|
75
|
+
class="rounded bg-slate-950/70 touch-none select-none cursor-crosshair"
|
|
76
|
+
{onpointerdown}
|
|
77
|
+
{onpointermove}
|
|
78
|
+
onpointerup={() => (drag = null)}
|
|
79
|
+
onpointercancel={() => (drag = null)}
|
|
80
|
+
{ondblclick}
|
|
81
|
+
>
|
|
82
|
+
{#if histogram && histogram.samples > 0}
|
|
83
|
+
<path d={histogramPath(histogram, histChannel, BOX, BOX)} fill="rgba(148, 163, 184, 0.25)" />
|
|
84
|
+
{/if}
|
|
85
|
+
{#each [64, 128, 192] as v (v)}
|
|
86
|
+
<g stroke="rgba(148, 163, 184, 0.2)" stroke-width="1">
|
|
87
|
+
<line x1={v} y1="0" x2={v} y2={BOX} />
|
|
88
|
+
<line x1="0" y1={v} x2={BOX} y2={v} />
|
|
89
|
+
</g>
|
|
90
|
+
{/each}
|
|
91
|
+
<line x1="0" y1={BOX} x2={BOX} y2="0" stroke="rgba(148, 163, 184, 0.35)" stroke-dasharray="4 4" />
|
|
92
|
+
<path d={curveSvgPath(points, BOX, BOX)} fill="none" stroke={STROKE[channel]} stroke-width="2" />
|
|
93
|
+
{#each points as p, i (i)}
|
|
94
|
+
{@const b = curvePointToBox(p, BOX, BOX)}
|
|
95
|
+
<circle
|
|
96
|
+
cx={b.x}
|
|
97
|
+
cy={b.y}
|
|
98
|
+
r="5"
|
|
99
|
+
fill={drag === i ? STROKE[channel] : '#0f172a'}
|
|
100
|
+
stroke={STROKE[channel]}
|
|
101
|
+
stroke-width="2"
|
|
102
|
+
aria-label="Point {p.input} to {p.output}"
|
|
103
|
+
/>
|
|
104
|
+
{/each}
|
|
105
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CurvePoint, ImageHistogram } from '@pixerate/editor/image-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
points: CurvePoint[];
|
|
4
|
+
/** `commit` is true for the first change of a gesture (one undo step), false for the drag moves. */
|
|
5
|
+
onchange: (points: CurvePoint[], commit: boolean) => void;
|
|
6
|
+
histogram?: ImageHistogram | null;
|
|
7
|
+
channel?: 'master' | 'red' | 'green' | 'blue';
|
|
8
|
+
size?: number;
|
|
9
|
+
}
|
|
10
|
+
declare const CurvesEditor: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type CurvesEditor = ReturnType<typeof CurvesEditor>;
|
|
12
|
+
export default CurvesEditor;
|
|
13
|
+
//# sourceMappingURL=CurvesEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CurvesEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/CurvesEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAa9E,UAAU,KAAK;IACb,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,oGAAoG;IACpG,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAsEH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ImageHistogram } from '@pixerate/editor/image-editor';
|
|
3
|
+
import { histogramPath } from '@pixerate/editor/image-editor';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
histogram: ImageHistogram | null;
|
|
7
|
+
/** `rgb` overlays the three channels on the luminance; a single channel draws just that one. */
|
|
8
|
+
channel?: 'rgb' | 'luminance' | 'red' | 'green' | 'blue';
|
|
9
|
+
height?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { histogram, channel = 'rgb', height = 64, class: className = '' }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const W = 256;
|
|
16
|
+
const FILL: Record<'luminance' | 'red' | 'green' | 'blue', string> = {
|
|
17
|
+
luminance: 'rgba(148, 163, 184, 0.45)',
|
|
18
|
+
red: 'rgba(239, 68, 68, 0.45)',
|
|
19
|
+
green: 'rgba(34, 197, 94, 0.45)',
|
|
20
|
+
blue: 'rgba(59, 130, 246, 0.45)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const channels = $derived(channel === 'rgb' ? (['luminance', 'red', 'green', 'blue'] as const) : ([channel] as const));
|
|
24
|
+
const paths = $derived(
|
|
25
|
+
histogram && histogram.samples > 0 ? channels.map((c) => ({ c, d: histogramPath(histogram, c, W, height) })) : []
|
|
26
|
+
);
|
|
27
|
+
const clipLow = $derived(histogram && histogram.samples ? Math.max(...histogram.shadows) / histogram.samples : 0);
|
|
28
|
+
const clipHigh = $derived(histogram && histogram.samples ? Math.max(...histogram.highlights) / histogram.samples : 0);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<svg
|
|
32
|
+
viewBox="0 0 {W} {height}"
|
|
33
|
+
preserveAspectRatio="none"
|
|
34
|
+
role="img"
|
|
35
|
+
aria-label={histogram ? `Histogram of ${histogram.samples} pixels` : 'Histogram (no data)'}
|
|
36
|
+
data-testid="image-editor-histogram"
|
|
37
|
+
class="w-full rounded bg-slate-950/60 {className}"
|
|
38
|
+
style="height: {height}px"
|
|
39
|
+
>
|
|
40
|
+
{#each paths as p (p.c)}
|
|
41
|
+
<path d={p.d} fill={FILL[p.c]} stroke="none" />
|
|
42
|
+
{/each}
|
|
43
|
+
{#if clipLow > 0.005}
|
|
44
|
+
<rect x="0" y="0" width="3" {height} fill="#3b82f6" data-testid="histogram-clip-shadows" />
|
|
45
|
+
{/if}
|
|
46
|
+
{#if clipHigh > 0.005}
|
|
47
|
+
<rect x={W - 3} y="0" width="3" {height} fill="#ef4444" data-testid="histogram-clip-highlights" />
|
|
48
|
+
{/if}
|
|
49
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ImageHistogram } from '@pixerate/editor/image-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
histogram: ImageHistogram | null;
|
|
4
|
+
/** `rgb` overlays the three channels on the luminance; a single channel draws just that one. */
|
|
5
|
+
channel?: 'rgb' | 'luminance' | 'red' | 'green' | 'blue';
|
|
6
|
+
height?: number;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const Histogram: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Histogram = ReturnType<typeof Histogram>;
|
|
11
|
+
export default Histogram;
|
|
12
|
+
//# sourceMappingURL=Histogram.svelte.d.ts.map
|