added visual setting for border padding filter
This commit is contained in:
parent
105691815d
commit
1126b2525f
2 changed files with 150 additions and 4 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
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';
|
import BrightnessIndicator from './visual/BrightnessIndicator.svelte';
|
||||||
|
import PaddingIndicator from './visual/PaddingIndicator.svelte';
|
||||||
|
|
||||||
let { disabled = false } = $props();
|
let { disabled = false } = $props();
|
||||||
|
|
||||||
|
|
@ -216,13 +217,13 @@
|
||||||
bind:checked={config.processing.crop.enabled}
|
bind:checked={config.processing.crop.enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-hint">Faces too close to the edge of the photo are often distorted and require too much padding</div>
|
<div class="section-hint">Padding happens when a face is at the border of a photo. It is required to move the subject to the center of the frame.</div>
|
||||||
|
|
||||||
{#if config.processing.crop.enabled}
|
{#if config.processing.crop.enabled}
|
||||||
<div class="setting-row sub-setting">
|
<div class="setting-row sub-setting no-border">
|
||||||
<label for="max-padding">
|
<label for="max-padding">
|
||||||
<span class="setting-label">Max Padding</span>
|
<span class="setting-label">Max Padding</span>
|
||||||
<span class="setting-hint">Maximum allowed padding from edge replication</span>
|
<span class="setting-hint">Maximum allowed padding</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="setting-control">
|
<div class="setting-control">
|
||||||
<input
|
<input
|
||||||
|
|
@ -236,6 +237,9 @@
|
||||||
<span class="value">{config.processing.crop.max_padding_percent}%</span>
|
<span class="value">{config.processing.crop.max_padding_percent}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="padding-indicator-container">
|
||||||
|
<PaddingIndicator maxPaddingPercent={config.processing.crop.max_padding_percent} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -738,7 +742,8 @@
|
||||||
border-bottom: 1px solid #252525;
|
border-bottom: 1px solid #252525;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row:last-child {
|
.setting-row:last-child,
|
||||||
|
.setting-row.no-border {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -992,6 +997,16 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.padding-indicator-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: #0f0f0f;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.brightness-indicator-container {
|
.brightness-indicator-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -1000,4 +1015,5 @@
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
130
frontend/src/lib/components/visual/PaddingIndicator.svelte
Normal file
130
frontend/src/lib/components/visual/PaddingIndicator.svelte
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Visual indicator for the max padding threshold.
|
||||||
|
* Shows two nested squares: the full output frame (outer) and the
|
||||||
|
* minimum real-pixel region (inner). The border zone between them
|
||||||
|
* represents padding (duplicated edge pixels) and is tinted red.
|
||||||
|
*
|
||||||
|
* @param {number} maxPaddingPercent - Max allowed padding percent (5-80)
|
||||||
|
*/
|
||||||
|
let { maxPaddingPercent = 30 } = $props();
|
||||||
|
|
||||||
|
const frameSize = 100;
|
||||||
|
const svgSize = 116;
|
||||||
|
const frameOffset = (svgSize - frameSize) / 2;
|
||||||
|
|
||||||
|
// A padding fraction of P% means up to P% of the area can be outside the
|
||||||
|
// original photo. For a uniform border: area_fraction = 1 - inner_side².
|
||||||
|
// So inner_side_fraction = sqrt(1 - P/100).
|
||||||
|
const innerSideFraction = $derived(Math.sqrt(Math.max(0, 1 - maxPaddingPercent / 100)));
|
||||||
|
const innerSize = $derived(frameSize * innerSideFraction);
|
||||||
|
const border = $derived((frameSize - innerSize) / 2);
|
||||||
|
|
||||||
|
const innerX = $derived(frameOffset + border);
|
||||||
|
const innerY = $derived(frameOffset + border);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="padding-indicator-wrap">
|
||||||
|
<svg viewBox="0 0 {svgSize} {svgSize}" class="padding-svg" role="img" aria-label="Padding threshold visualization">
|
||||||
|
<!-- Outer square: full output frame -->
|
||||||
|
<rect
|
||||||
|
x={frameOffset}
|
||||||
|
y={frameOffset}
|
||||||
|
width={frameSize}
|
||||||
|
height={frameSize}
|
||||||
|
fill="#2a2a2a"
|
||||||
|
stroke="#666"
|
||||||
|
stroke-width="1.5"
|
||||||
|
rx="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Padding zone: red tint over the border area -->
|
||||||
|
<rect
|
||||||
|
x={frameOffset}
|
||||||
|
y={frameOffset}
|
||||||
|
width={frameSize}
|
||||||
|
height={frameSize}
|
||||||
|
fill="rgba(239, 68, 68, 0.30)"
|
||||||
|
rx="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Inner square: real image pixels region -->
|
||||||
|
{#if innerSize > 2}
|
||||||
|
<rect
|
||||||
|
x={innerX}
|
||||||
|
y={innerY}
|
||||||
|
width={innerSize}
|
||||||
|
height={innerSize}
|
||||||
|
fill="#3a3a3a"
|
||||||
|
/>
|
||||||
|
<!-- Dashed border to distinguish inner from outer -->
|
||||||
|
<rect
|
||||||
|
x={innerX}
|
||||||
|
y={innerY}
|
||||||
|
width={innerSize}
|
||||||
|
height={innerSize}
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255,255,255,0.35)"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke-dasharray="3 2"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="swatch real"></span>
|
||||||
|
real image pixels
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="swatch padding"></span>
|
||||||
|
padding (duplicated edge pixels)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.padding-indicator-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-svg {
|
||||||
|
width: 116px;
|
||||||
|
height: 116px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 1px solid #666;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch.real {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch.padding {
|
||||||
|
background: color-mix(in srgb, rgb(239, 68, 68) 30%, #2a2a2a);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue