Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
import { ImageCanvas, Sidebar, PixelInspector, FormulaPanel, LoadingSkeleton, ProcessingIndicator } from './components';
import { ImageCanvas, Sidebar, PixelInspector, FormulaPanel, LoadingSkeleton, ProcessingIndicator, StatusBar } from './components';
import { useHistory, useImageWorker } from './hooks';
import type { FilterType, FilterParams } from './types';
import {
Expand Down Expand Up @@ -52,6 +52,7 @@ import {
applySepia,
applySwapChannels,
} from './utils/colorFilters';
import { applyFFTSpectrum } from './utils/fft';
import './App.css';

// =============================================================================
Expand Down Expand Up @@ -137,6 +138,9 @@ function processImageSync(
return applySepia(imageData);
case 'swapChannels':
return applySwapChannels(imageData);
// Frequency Domain
case 'fftSpectrum':
return applyFFTSpectrum(imageData);
case 'none':
default:
return imageData;
Expand Down Expand Up @@ -865,6 +869,13 @@ function App() {
originalHistogramData={originalHistogramData}
hasImage={!!originalImage}
/>

{/* Status Bar: Image Statistics */}
<StatusBar
imageData={displayImage}
originalData={originalImage}
showOriginal={showOriginal}
/>
</main>
</div>
);
Expand Down
14 changes: 14 additions & 0 deletions src/components/FormulaPanel/FormulaPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,20 @@
],
};

// Frequency Domain
case 'fftSpectrum':
return {
name: '2D Fourier Transform (FFT)',
latex: 'F(u,v) = \\sum_{x=0}^{M-1} \\sum_{y=0}^{N-1} f(x,y) \\cdot e^{-j2\\pi(ux/M + vy/N)}',
description: 'Decomposes image into frequency components. Center shows low frequencies (smooth areas), edges show high frequencies (details/edges).',
variables: [
{ symbol: 'F(u,v)', meaning: 'Frequency component' },
{ symbol: 'f(x,y)', meaning: 'Spatial pixel value' },
{ symbol: 'u, v', meaning: 'Frequency coordinates' },
{ symbol: 'M, N', meaning: 'Image dimensions' },
],
};

case 'none':
default:
return {
Expand Down Expand Up @@ -367,7 +381,7 @@
displayMode: true,
throwOnError: false,
});
} catch (e) {

Check failure on line 384 in src/components/FormulaPanel/FormulaPanel.tsx

View workflow job for this annotation

GitHub Actions / Test

'e' is defined but never used
formulaRef.current.textContent = formulaInfo.latex;
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,18 @@ const FILTER_CATEGORIES: FilterCategory[] = [
},
],
},
{
id: 'frequency',
name: 'Frequency Domain',
filters: [
{
id: 'fftSpectrum',
name: 'FFT Spectrum',
description: 'Visualize frequency components',
formula: 'F(u,v) = \\sum f \\cdot e^{-j2\\pi(ux/M + vy/N)}',
},
],
},
];

// =============================================================================
Expand Down
159 changes: 159 additions & 0 deletions src/components/StatusBar/StatusBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* StatusBar - Styles
*
* Compact horizontal bar displaying image statistics.
* Dark theme with subtle separators and hover effects.
*/

/* =============================================================================
Container
============================================================================= */

.status-bar {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xs) var(--space-lg);
background: var(--color-bg-secondary);
border-top: 1px solid var(--color-border);
height: 32px;
min-height: 32px;
font-size: 0.8125rem;
font-family: var(--font-mono);
user-select: none;
}

.status-bar--empty {
justify-content: center;
}

.status-bar__placeholder {
color: var(--color-text-muted);
font-family: var(--font-sans);
font-style: italic;
font-size: 0.75rem;
}

/* =============================================================================
Items
============================================================================= */

.status-bar__item {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: 2px 6px;
border-radius: var(--radius-sm);
transition: background-color 0.15s ease;
cursor: default;
}

.status-bar__item:hover {
background: var(--color-bg-tertiary);
}

.status-bar__label {
color: var(--color-accent-primary);
font-weight: 600;
font-size: 0.875rem;
}

.status-bar__value {
color: var(--color-text-primary);
font-weight: 500;
}

.status-bar__unit {
color: var(--color-text-muted);
font-size: 0.6875rem;
margin-left: 1px;
}

/* =============================================================================
Dividers
============================================================================= */

.status-bar__divider {
width: 1px;
height: 14px;
background: var(--color-border);
opacity: 0.5;
}

/* =============================================================================
Special Items
============================================================================= */

/* Entropy - highlight with gradient */
.status-bar__item--entropy .status-bar__label {
background: linear-gradient(135deg, #10b981, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

/* Range - subtle different color */
.status-bar__item--range .status-bar__label {
color: var(--color-text-secondary);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}

/* Pixels - muted */
.status-bar__item--pixels .status-bar__label {
color: var(--color-text-secondary);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}

/* =============================================================================
Original Indicator
============================================================================= */

.status-bar__original-indicator {
margin-left: auto;
padding: 2px 8px;
background: rgba(245, 158, 11, 0.15);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: var(--radius-sm);
color: #f59e0b;
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 1px;
animation: pulse 1.5s ease-in-out infinite;
}

/* =============================================================================
Responsive
============================================================================= */

@media (max-width: 900px) {
.status-bar {
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
font-size: 0.75rem;
}

.status-bar__item {
padding: 2px 4px;
gap: 2px;
}

/* Hide less important items on small screens */
.status-bar__item--pixels,
.status-bar__item--range {
display: none;
}

.status-bar__divider:nth-last-child(-n+4) {
display: none;
}
}

@media (max-width: 600px) {
.status-bar__unit {
display: none;
}
}
121 changes: 121 additions & 0 deletions src/components/StatusBar/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* StatusBar - Image Statistics Display
*
* Displays real-time statistics about the current image
* in a compact bar at the bottom of the application.
*
* @module StatusBar
* @author ImageVisLab Contributors
* @license MIT
*/

import { useMemo } from 'react';
import { calculateImageStatistics, formatPixelCount, type ImageStatistics } from '../../utils/imageStatistics';
import './StatusBar.css';

// =============================================================================
// Types
// =============================================================================

interface StatusBarProps {
/** The image data to calculate statistics from */
imageData: ImageData | null;
/** Whether to show original image stats (when Space is held) */
originalData?: ImageData | null;
/** Whether currently showing original */
showOriginal?: boolean;
}

// =============================================================================
// Component
// =============================================================================

/**
* StatusBar displays image statistics in a compact horizontal bar.
*
* Statistics shown:
* - μ (Mean): Average intensity
* - σ (Std Dev): Intensity spread
* - H (Entropy): Information content in bits
* - Min/Max: Intensity range
* - Pixel count: Total pixels
*/
export function StatusBar({ imageData, originalData, showOriginal }: StatusBarProps) {
// Calculate statistics (memoized for performance)
const stats = useMemo<ImageStatistics | null>(() => {
const data = showOriginal && originalData ? originalData : imageData;
if (!data) return null;
return calculateImageStatistics(data);
}, [imageData, originalData, showOriginal]);

// Don't render if no image
if (!stats) {
return (
<div className="status-bar status-bar--empty">
<span className="status-bar__placeholder">
Carregue uma imagem para ver estatísticas
</span>
</div>
);
}

return (
<div className="status-bar" role="status" aria-label="Estatísticas da imagem">
{/* Mean */}
<div className="status-bar__item" title="Média (Mean): Brilho médio da imagem">
<span className="status-bar__label">μ</span>
<span className="status-bar__value">{stats.mean.toFixed(1)}</span>
</div>

<div className="status-bar__divider" />

{/* Standard Deviation */}
<div className="status-bar__item" title="Desvio Padrão: Medida de contraste da imagem">
<span className="status-bar__label">σ</span>
<span className="status-bar__value">{stats.stdDev.toFixed(1)}</span>
</div>

<div className="status-bar__divider" />

{/* Entropy */}
<div className="status-bar__item status-bar__item--entropy" title="Entropia: Quantidade de informação (0-8 bits)">
<span className="status-bar__label">H</span>
<span className="status-bar__value">{stats.entropy.toFixed(2)}</span>
<span className="status-bar__unit">bits</span>
</div>

<div className="status-bar__divider" />

{/* Min/Max */}
<div className="status-bar__item status-bar__item--range" title="Faixa de intensidade (mínimo/máximo)">
<span className="status-bar__label">Range</span>
<span className="status-bar__value">{stats.min}–{stats.max}</span>
</div>

<div className="status-bar__divider" />

{/* Variance (more compact) */}
<div className="status-bar__item" title="Variância: σ² - Dispersão dos valores de intensidade">
<span className="status-bar__label">σ²</span>
<span className="status-bar__value">{stats.variance.toFixed(0)}</span>
</div>

<div className="status-bar__divider" />

{/* Pixel Count */}
<div className="status-bar__item status-bar__item--pixels" title={`Total de pixels: ${stats.pixelCount.toLocaleString()}`}>
<span className="status-bar__label">Pixels</span>
<span className="status-bar__value">{formatPixelCount(stats.pixelCount)}</span>
</div>

{/* Original indicator */}
{showOriginal && (
<div className="status-bar__original-indicator">
ORIGINAL
</div>
)}
</div>
);
}

export default StatusBar;
2 changes: 2 additions & 0 deletions src/components/StatusBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { StatusBar } from './StatusBar';
export { default } from './StatusBar';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export { FormulaPanel } from './FormulaPanel';
export { LaTeXFormula } from './LaTeXFormula';
export { ErrorBoundary } from './ErrorBoundary';
export { LoadingSkeleton, ProcessingIndicator } from './LoadingSkeleton';
export { StatusBar } from './StatusBar';
4 changes: 3 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export type FilterType =
| 'closing'
// Custom Operations
| 'customFormula'
| 'customKernel';
| 'customKernel'
// Frequency Domain
| 'fftSpectrum';

// =============================================================================
// Application State Types
Expand Down
Loading
Loading