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 EyeIndicator from './visual/EyeIndicator.svelte';
|
||||
import AlignmentIndicator from './visual/AlignmentIndicator.svelte';
|
||||
import BrightnessIndicator from './visual/BrightnessIndicator.svelte';
|
||||
|
||||
let { disabled = false } = $props();
|
||||
|
||||
|
|
@ -192,39 +193,15 @@
|
|||
</div>
|
||||
|
||||
{#if config.processing.brightness.enabled}
|
||||
<div class="setting-row sub-setting">
|
||||
<label for="min-brightness">
|
||||
<span class="setting-label">Min Brightness</span>
|
||||
<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 class="brightness-visual-control sub-setting">
|
||||
<div class="brightness-hint">
|
||||
Drag the markers to set the acceptable brightness range
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row sub-setting">
|
||||
<label for="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"
|
||||
<div class="brightness-indicator-container">
|
||||
<BrightnessIndicator
|
||||
bind:minBrightness={config.processing.brightness.min_brightness}
|
||||
bind:maxBrightness={config.processing.brightness.max_brightness}
|
||||
/>
|
||||
<span class="value">{config.processing.brightness.max_brightness.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -316,7 +293,7 @@
|
|||
<!-- Alignment Section -->
|
||||
<div class="setting-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">Final image composition</span>
|
||||
<span class="section-title">Final Image Composition</span>
|
||||
</div>
|
||||
<div class="setting-row sub-setting">
|
||||
<label for="eye-distance">
|
||||
|
|
@ -749,4 +726,26 @@
|
|||
width: 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>
|
||||
|
|
|
|||
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