Better brightness filtering UI
This commit is contained in:
parent
8b14ae88a0
commit
9d313382eb
2 changed files with 289 additions and 32 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
import { handleError } from '../errorHandler.js';
|
import { handleError } from '../errorHandler.js';
|
||||||
import EyeIndicator from './visual/EyeIndicator.svelte';
|
import EyeIndicator from './visual/EyeIndicator.svelte';
|
||||||
import AlignmentIndicator from './visual/AlignmentIndicator.svelte';
|
import AlignmentIndicator from './visual/AlignmentIndicator.svelte';
|
||||||
|
import BrightnessIndicator from './visual/BrightnessIndicator.svelte';
|
||||||
|
|
||||||
let { disabled = false } = $props();
|
let { disabled = false } = $props();
|
||||||
|
|
||||||
|
|
@ -192,39 +193,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if config.processing.brightness.enabled}
|
{#if config.processing.brightness.enabled}
|
||||||
<div class="setting-row sub-setting">
|
<div class="brightness-visual-control sub-setting">
|
||||||
<label for="min-brightness">
|
<div class="brightness-hint">
|
||||||
<span class="setting-label">Min Brightness</span>
|
Drag the markers to set the acceptable brightness range
|
||||||
<span class="setting-hint">Skip images darker than this</span>
|
|
||||||
</label>
|
|
||||||
<div class="setting-control">
|
|
||||||
<input
|
|
||||||
id="min-brightness"
|
|
||||||
type="range"
|
|
||||||
bind:value={config.processing.brightness.min_brightness}
|
|
||||||
min="0"
|
|
||||||
max="0.5"
|
|
||||||
step="0.05"
|
|
||||||
/>
|
|
||||||
<span class="value">{config.processing.brightness.min_brightness.toFixed(2)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="brightness-indicator-container">
|
||||||
|
<BrightnessIndicator
|
||||||
<div class="setting-row sub-setting">
|
bind:minBrightness={config.processing.brightness.min_brightness}
|
||||||
<label for="max-brightness">
|
bind:maxBrightness={config.processing.brightness.max_brightness}
|
||||||
<span class="setting-label">Max Brightness</span>
|
|
||||||
<span class="setting-hint">Skip images brighter than this</span>
|
|
||||||
</label>
|
|
||||||
<div class="setting-control">
|
|
||||||
<input
|
|
||||||
id="max-brightness"
|
|
||||||
type="range"
|
|
||||||
bind:value={config.processing.brightness.max_brightness}
|
|
||||||
min="0.5"
|
|
||||||
max="1.0"
|
|
||||||
step="0.05"
|
|
||||||
/>
|
/>
|
||||||
<span class="value">{config.processing.brightness.max_brightness.toFixed(2)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -316,7 +293,7 @@
|
||||||
<!-- Alignment Section -->
|
<!-- Alignment Section -->
|
||||||
<div class="setting-section">
|
<div class="setting-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-title">Final image composition</span>
|
<span class="section-title">Final Image Composition</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row sub-setting">
|
<div class="setting-row sub-setting">
|
||||||
<label for="eye-distance">
|
<label for="eye-distance">
|
||||||
|
|
@ -749,4 +726,26 @@
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Brightness visual control */
|
||||||
|
.brightness-visual-control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brightness-hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brightness-indicator-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #0f0f0f;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
258
frontend/src/lib/components/visual/BrightnessIndicator.svelte
Normal file
258
frontend/src/lib/components/visual/BrightnessIndicator.svelte
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Interactive brightness filter control.
|
||||||
|
* Displays a gradient from black to white with draggable markers.
|
||||||
|
*
|
||||||
|
* @param {number} minBrightness - Minimum accepted brightness (0-1)
|
||||||
|
* @param {number} maxBrightness - Maximum accepted brightness (0-1)
|
||||||
|
*/
|
||||||
|
let { minBrightness = $bindable(0.1), maxBrightness = $bindable(0.95) } = $props();
|
||||||
|
|
||||||
|
const width = 240;
|
||||||
|
const height = 80;
|
||||||
|
const gradientBarHeight = 30;
|
||||||
|
const gradientBarY = 15;
|
||||||
|
|
||||||
|
// Convert brightness values (0-1) to x positions
|
||||||
|
const minX = $derived(minBrightness * width);
|
||||||
|
const maxX = $derived(maxBrightness * width);
|
||||||
|
|
||||||
|
let dragging = $state(null); // 'min' | 'max' | null
|
||||||
|
let svgElement = $state(null);
|
||||||
|
|
||||||
|
function handlePointerDown(e, type) {
|
||||||
|
e.preventDefault();
|
||||||
|
dragging = type;
|
||||||
|
svgElement.setPointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(e) {
|
||||||
|
if (!dragging || !svgElement) return;
|
||||||
|
|
||||||
|
const rect = svgElement.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const scaleX = width / rect.width;
|
||||||
|
const actualX = x * scaleX;
|
||||||
|
|
||||||
|
// Clamp to valid range
|
||||||
|
let newValue = Math.max(0, Math.min(1, actualX / width));
|
||||||
|
|
||||||
|
// Round to nearest 0.05
|
||||||
|
newValue = Math.round(newValue * 20) / 20;
|
||||||
|
|
||||||
|
if (dragging === 'min') {
|
||||||
|
// Ensure min doesn't exceed max - 0.1
|
||||||
|
minBrightness = Math.min(newValue, maxBrightness - 0.1);
|
||||||
|
} else if (dragging === 'max') {
|
||||||
|
// Ensure max doesn't go below min + 0.1
|
||||||
|
maxBrightness = Math.max(newValue, minBrightness + 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(e) {
|
||||||
|
if (dragging && svgElement) {
|
||||||
|
svgElement.releasePointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
dragging = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
bind:this={svgElement}
|
||||||
|
viewBox="0 0 {width} {height}"
|
||||||
|
class="brightness-indicator"
|
||||||
|
class:dragging={dragging !== null}
|
||||||
|
role="group"
|
||||||
|
aria-label="Brightness range control"
|
||||||
|
onpointermove={handlePointerMove}
|
||||||
|
onpointerup={handlePointerUp}
|
||||||
|
onpointerleave={handlePointerUp}
|
||||||
|
>
|
||||||
|
<!-- Define gradient from black to white -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="brightness-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" style="stop-color:rgb(0,0,0);stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:rgb(255,255,255);stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background gradient bar -->
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y={gradientBarY}
|
||||||
|
width={width}
|
||||||
|
height={gradientBarHeight}
|
||||||
|
fill="url(#brightness-gradient)"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="1.5"
|
||||||
|
rx="3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Rejected range overlays (darker areas) -->
|
||||||
|
<!-- Left rejected area (too dark) -->
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y={gradientBarY}
|
||||||
|
width={minX}
|
||||||
|
height={gradientBarHeight}
|
||||||
|
fill="rgba(255, 0, 0, 0.3)"
|
||||||
|
class="rejected-area"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Right rejected area (too bright) -->
|
||||||
|
<rect
|
||||||
|
x={maxX}
|
||||||
|
y={gradientBarY}
|
||||||
|
width={width - maxX}
|
||||||
|
height={gradientBarHeight}
|
||||||
|
fill="rgba(255, 0, 0, 0.3)"
|
||||||
|
class="rejected-area"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Min brightness marker -->
|
||||||
|
<g
|
||||||
|
class="marker"
|
||||||
|
class:active={dragging === 'min'}
|
||||||
|
onpointerdown={(e) => handlePointerDown(e, 'min')}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<!-- Invisible wider hit area for easier grabbing -->
|
||||||
|
<rect
|
||||||
|
x={minX - 10}
|
||||||
|
y={gradientBarY - 10}
|
||||||
|
width="20"
|
||||||
|
height={gradientBarHeight + 20}
|
||||||
|
fill="transparent"
|
||||||
|
class="hit-area"
|
||||||
|
/>
|
||||||
|
<!-- Vertical line -->
|
||||||
|
<line
|
||||||
|
x1={minX}
|
||||||
|
y1={gradientBarY - 5}
|
||||||
|
x2={minX}
|
||||||
|
y2={gradientBarY + gradientBarHeight + 5}
|
||||||
|
stroke="#4f46e5"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<!-- Triangle at top -->
|
||||||
|
<path
|
||||||
|
d="M {minX - 4} {gradientBarY - 5} L {minX + 4} {gradientBarY - 5} L {minX} {gradientBarY - 1} Z"
|
||||||
|
fill="#4f46e5"
|
||||||
|
/>
|
||||||
|
<!-- Triangle at bottom -->
|
||||||
|
<path
|
||||||
|
d="M {minX - 4} {gradientBarY + gradientBarHeight + 5} L {minX + 4} {gradientBarY + gradientBarHeight + 5} L {minX} {gradientBarY + gradientBarHeight + 1} Z"
|
||||||
|
fill="#4f46e5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Max brightness marker -->
|
||||||
|
<g
|
||||||
|
class="marker"
|
||||||
|
class:active={dragging === 'max'}
|
||||||
|
onpointerdown={(e) => handlePointerDown(e, 'max')}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<!-- Invisible wider hit area for easier grabbing -->
|
||||||
|
<rect
|
||||||
|
x={maxX - 10}
|
||||||
|
y={gradientBarY - 10}
|
||||||
|
width="20"
|
||||||
|
height={gradientBarHeight + 20}
|
||||||
|
fill="transparent"
|
||||||
|
class="hit-area"
|
||||||
|
/>
|
||||||
|
<!-- Vertical line -->
|
||||||
|
<line
|
||||||
|
x1={maxX}
|
||||||
|
y1={gradientBarY - 5}
|
||||||
|
x2={maxX}
|
||||||
|
y2={gradientBarY + gradientBarHeight + 5}
|
||||||
|
stroke="#4f46e5"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<!-- Triangle at top -->
|
||||||
|
<path
|
||||||
|
d="M {maxX - 4} {gradientBarY - 5} L {maxX + 4} {gradientBarY - 5} L {maxX} {gradientBarY - 1} Z"
|
||||||
|
fill="#4f46e5"
|
||||||
|
/>
|
||||||
|
<!-- Triangle at bottom -->
|
||||||
|
<path
|
||||||
|
d="M {maxX - 4} {gradientBarY + gradientBarHeight + 5} L {maxX + 4} {gradientBarY + gradientBarHeight + 5} L {maxX} {gradientBarY + gradientBarHeight + 1} Z"
|
||||||
|
fill="#4f46e5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Value labels -->
|
||||||
|
<text
|
||||||
|
x={minX}
|
||||||
|
y={gradientBarY + gradientBarHeight + 20}
|
||||||
|
text-anchor="middle"
|
||||||
|
class="value-label"
|
||||||
|
>
|
||||||
|
{minBrightness.toFixed(2)}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={maxX}
|
||||||
|
y={gradientBarY + gradientBarHeight + 20}
|
||||||
|
text-anchor="middle"
|
||||||
|
class="value-label"
|
||||||
|
>
|
||||||
|
{maxBrightness.toFixed(2)}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.brightness-indicator {
|
||||||
|
width: 240px;
|
||||||
|
height: 80px;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brightness-indicator.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker {
|
||||||
|
cursor: grab;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker:hover line,
|
||||||
|
.marker:hover path {
|
||||||
|
stroke: #6366f1;
|
||||||
|
fill: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker.active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker.active line,
|
||||||
|
.marker.active path {
|
||||||
|
stroke: #818cf8;
|
||||||
|
fill: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hit-area {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rejected-area {
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-label {
|
||||||
|
fill: #4f46e5;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue