Skip to content

Commit 25bf243

Browse files
committed
feat: major UI overhaul — dynamic lenses, draggable overlays, new scenes
- Refactored from fixed 2-lens to dynamic 1-3 lens system with add/remove - FOV rectangles drawn at natural size relative to 14mm FF reference - Draggable overlays with mouse and touch support, clamped to photo bounds - Replaced sample images: landscape lake, portrait, bird/wildlife, city, milky way - Added landscape/portrait orientation toggle - Removed side-by-side mode, frame ruler, keyboard shortcuts overlay - Mobile responsive: full-width canvas, stacked layout, touch drag - Added center overlays button, reset button - Logarithmic focal length slider with snap-to-preset - Updated README with feature descriptions and screenshot - Added tests, linting, CLAUDE.md
1 parent 938a2ba commit 25bf243

39 files changed

Lines changed: 2686 additions & 450 deletions

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ jobs:
2828
cache: npm
2929

3030
- run: npm ci
31+
- run: npm audit --omit=dev
32+
- run: npm run lint
33+
- run: npm test
3134
- run: npm run build
3235

3336
- uses: actions/configure-pages@v4

.gitignore

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
node_modules
22
dist
3-
.superpowers
3+
.superpowers
4+
5+
# Editor / OS
6+
.DS_Store
7+
*.swp
8+
*.swo
9+
.idea
10+
.vscode
11+
*.local
12+
13+
# Environment
14+
.env
15+
.env.*
16+
17+
# Build artifacts
18+
*.tsbuildinfo

CLAUDE.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# CLAUDE.md
2+
3+
## Project Overview
4+
5+
FOV Viewer is a static React app for comparing camera focal lengths and visualizing field of view. Deployed to GitHub Pages. Educational focus for photography learners.
6+
7+
## Tech Stack
8+
9+
- React 19 + TypeScript + Vite
10+
- Vitest + jsdom + @testing-library/react + @testing-library/jest-dom
11+
- ESLint with typescript-eslint
12+
- No component library — custom CSS with CSS custom properties
13+
- Canvas API for image rendering (overlay rectangles)
14+
- Zero runtime dependencies beyond React
15+
16+
## Commands
17+
18+
- `npm run dev` — start dev server at `http://localhost:5173/fov-viewer/`
19+
- `npm run build` — type-check + production build to `dist/`
20+
- `npm test` — run Vitest tests
21+
- `npm run test:watch` — run tests in watch mode
22+
- `npm run lint` — run ESLint
23+
24+
## Architecture
25+
26+
- **State**: single `useReducer` in `App.tsx` with `lenses[]` array (up to 3), synced bidirectionally with URL query params via `useQuerySync`
27+
- **Rendering**: `<canvas>` element draws images + overlay rectangles. Supports landscape/portrait orientation.
28+
- **Theming**: CSS custom properties on `[data-theme]` attribute. Dark default, persisted to localStorage.
29+
- **FOV math**: `src/utils/fov.ts` — standard rectilinear projection formula based on 36x24mm full-frame sensor.
30+
31+
## Key Files
32+
33+
- `src/App.tsx` — root component, state reducer, wires everything together
34+
- `src/types.ts` — shared types, DEFAULT_STATE, LENS_COLORS, MAX_LENSES
35+
- `src/components/Canvas.tsx` — main rendering logic (overlay mode)
36+
- `src/components/LensPanel.tsx` — focal length slider, presets, sensor select
37+
- `src/components/Sidebar.tsx` — sidebar layout wrapper
38+
- `src/components/SceneStrip.tsx` — scene thumbnail selector
39+
- `src/components/ActionBar.tsx` — copy image/link, reset buttons
40+
- `src/components/ThemeToggle.tsx` — dark/light theme toggle
41+
- `src/components/Toast.tsx` — notification popup
42+
- `src/utils/fov.ts` — FOV calculations (tests in `fov.test.ts`)
43+
- `src/utils/export.ts` — clipboard/download helpers (tests in `export.test.ts`)
44+
- `src/hooks/useQuerySync.ts` — URL query param sync (tests in `useQuerySync.test.ts`)
45+
- `src/data/sensors.ts` — 6 sensor presets (tests in `sensors.test.ts`)
46+
- `src/data/focalLengths.ts` — 12 focal length presets (tests in `focalLengths.test.ts`)
47+
- `src/data/scenes.ts` — 5 sample scene images
48+
- `src/reducer.test.ts` — App reducer state transition tests
49+
- `src/integration.test.ts` — cross-module integration tests
50+
- `src/test-setup.ts` — Vitest setup (jest-dom matchers)
51+
52+
## Conventions
53+
54+
- BEM-style CSS class names (e.g. `.lens-panel__header`)
55+
- Named exports for all components
56+
- Types in `src/types.ts`
57+
- No external UI libraries — keep it dependency-free
58+
- GitHub Pages base path: `/fov-viewer/` (set in `vite.config.ts`)
59+
- Vitest configured with jsdom environment in `vite.config.ts`
60+
- Test files live next to source files (`*.test.ts`) except cross-cutting tests at `src/` root

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# FOV Viewer
2+
3+
A browser-based tool that helps photographers visualize and compare field of view across different focal lengths and sensor sizes. Built for learning — understand how focal length and crop factor affect framing.
4+
5+
![FOV Viewer Screenshot](docs/screenshot.png)
6+
7+
## Features
8+
9+
### Compare Up to 3 Lenses
10+
Start with one lens and add up to two more. Each lens gets its own color-coded overlay rectangle drawn on the image, showing exactly what that focal length would capture. Remove lenses you don't need with the X button.
11+
12+
### Focal Length Control
13+
Continuous logarithmic slider from 14mm to 800mm with snap-to-preset behavior. Quick-select buttons for common focal lengths: 14mm, 20mm, 24mm, 35mm, 50mm, 85mm, 135mm, 200mm, 400mm, 600mm, 800mm.
14+
15+
### Sensor / Crop Factor Presets
16+
Choose from 6 sensor sizes per lens — Medium Format (0.79x), Full Frame (1.0x), APS-C Nikon/Sony (1.5x), APS-C Canon (1.6x), Micro Four Thirds (2.0x), and 1" Sensor (2.7x). Equivalent focal length is displayed automatically when using a crop sensor.
17+
18+
### Draggable Overlays
19+
Click and drag any FOV rectangle to reposition it on the image. Works on both desktop (mouse) and mobile (touch). Rectangles are constrained to stay within the photo boundaries. Hit "Center" to reset all positions.
20+
21+
### Landscape / Portrait Orientation
22+
Toggle between landscape and portrait orientation to see how FOV changes when you rotate the camera.
23+
24+
### 5 Sample Scenes
25+
Curated photos for different shooting scenarios — landscape, portrait, bird/wildlife, city street, and milky way/night sky. Each scene demonstrates different reasons you might choose one focal length over another.
26+
27+
### Shareable Links
28+
Every setting is encoded in the URL query parameters. Change any control and the URL updates in real time. Copy the link to share your exact comparison with someone else.
29+
30+
### Copy to Clipboard
31+
Export the current canvas view (image + overlay rectangles) as a PNG to your clipboard with one click. Falls back to file download if clipboard API isn't available.
32+
33+
### Dark / Light Theme
34+
Dark theme by default (easier on the eyes for photo work). Toggle with the sun/moon button. Preference is saved to localStorage.
35+
36+
### Responsive Design
37+
Full sidebar layout on desktop, stacked layout on mobile with full-width image canvas. Touch-draggable overlays on mobile.
38+
39+
## Tech Stack
40+
41+
- React 19 + TypeScript
42+
- Vite
43+
- Vitest + Testing Library
44+
- Canvas API for rendering
45+
- CSS custom properties for theming
46+
- Zero runtime dependencies beyond React
47+
48+
## Development
49+
50+
```bash
51+
npm install
52+
npm run dev
53+
```
54+
55+
Dev server runs at `http://localhost:5173/fov-viewer/`.
56+
57+
## Testing
58+
59+
```bash
60+
npm test # single run
61+
npm run test:watch # watch mode
62+
```
63+
64+
## Build
65+
66+
```bash
67+
npm run build
68+
```
69+
70+
Static output goes to `dist/`.
71+
72+
## Deployment
73+
74+
Push to `main` — GitHub Actions automatically builds and deploys to GitHub Pages.
75+
76+
To set up:
77+
1. Create a GitHub repo called `fov-viewer`
78+
2. Push this code to `main`
79+
3. Go to Settings > Pages > Source: "GitHub Actions"
80+
81+
## URL Parameters
82+
83+
All state is encoded in the URL for sharing:
84+
85+
| Param | Description | Example |
86+
|-------|-------------|---------|
87+
| `a` | Lens A focal length (mm) | `a=20` |
88+
| `b` | Lens B focal length (mm) | `b=85` |
89+
| `c` | Lens C focal length (mm) | `c=200` |
90+
| `sa` | Lens A sensor | `sa=ff` |
91+
| `sb` | Lens B sensor | `sb=apsc_n` |
92+
| `sc` | Lens C sensor | `sc=m43` |
93+
| `img` | Image index (0-4) | `img=0` |
94+
| `theme` | `dark` or `light` | `theme=dark` |
95+
96+
Sensor codes: `mf`, `ff`, `apsc_n`, `apsc_c`, `m43`, `1in`
97+
98+
## License
99+
100+
MIT

docs/SETUP.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# FOV Viewer — Local Development & Setup Guide
2+
3+
A focal-length comparison tool for photographers, built with React + TypeScript + Vite.
4+
5+
## Prerequisites
6+
7+
| Tool | Version | Check |
8+
|------|---------|-------|
9+
| Node.js | 20+ | `node -v` |
10+
| npm | 9+ | `npm -v` |
11+
| Git | any | `git --version` |
12+
13+
## Quick Start
14+
15+
```bash
16+
# Clone the repo
17+
git clone <repo-url> && cd fov-viewer
18+
19+
# Or run the automated setup script:
20+
./scripts/setup.sh
21+
```
22+
23+
## Manual Setup
24+
25+
```bash
26+
# 1. Install dependencies
27+
npm ci
28+
29+
# 2. Start dev server (http://localhost:5173/fov-viewer/)
30+
npm run dev
31+
32+
# 3. Open in browser
33+
open http://localhost:5173/fov-viewer/
34+
```
35+
36+
> **Note:** The app is served under the `/fov-viewer/` base path (matching the GitHub Pages deployment).
37+
38+
## Available Scripts
39+
40+
| Command | Description |
41+
|---------|-------------|
42+
| `npm run dev` | Start Vite dev server with HMR |
43+
| `npm run build` | Type-check and build for production (`dist/`) |
44+
| `npm run preview` | Serve the production build locally |
45+
| `npm run lint` | Run ESLint |
46+
| `npm run test` | Run tests once (Vitest) |
47+
| `npm run test:watch` | Run tests in watch mode |
48+
49+
## Project Structure
50+
51+
```
52+
src/
53+
├── main.tsx # Entry point
54+
├── App.tsx / App.css # Root component and styles
55+
├── theme.css # CSS custom properties (dark/light)
56+
├── components/ # UI components
57+
│ ├── Canvas.tsx # Image + FOV overlay rendering
58+
│ ├── LensPanel.tsx # Lens config card (focal length, sensor)
59+
│ ├── Sidebar.tsx # Desktop sidebar wrapper
60+
│ ├── SceneStrip.tsx # Scene thumbnail selector
61+
│ ├── ActionBar.tsx # Copy image / Copy link buttons
62+
│ ├── ThemeToggle.tsx # Dark/light theme toggle
63+
│ └── Toast.tsx # Notification popup
64+
├── hooks/
65+
│ └── useQuerySync.ts # State ↔ URL query param sync
66+
├── utils/
67+
│ ├── fov.ts # FOV math (angles, frame width)
68+
│ ├── fov.test.ts # Unit tests for FOV calculations
69+
│ └── export.ts # Canvas → clipboard/PNG export
70+
├── data/
71+
│ ├── sensors.ts # Sensor presets (crop factors)
72+
│ ├── focalLengths.ts # Focal length presets + labels
73+
│ └── scenes.ts # Scene image metadata
74+
└── assets/ # Sample images (landscape, portrait, etc.)
75+
```
76+
77+
## Deployment
78+
79+
The app deploys automatically to **GitHub Pages** on push to `main` via `.github/workflows/deploy.yml`:
80+
81+
1. `npm ci` + `npm run build`
82+
2. Uploads `dist/` as a GitHub Pages artifact
83+
3. Deploys to `https://<user>.github.io/fov-viewer/`
84+
85+
To test a production build locally:
86+
87+
```bash
88+
npm run build && npm run preview
89+
```
90+
91+
## Troubleshooting
92+
93+
| Issue | Fix |
94+
|-------|-----|
95+
| Blank page at `localhost:5173` | Navigate to `localhost:5173/fov-viewer/` (note the base path) |
96+
| `npm ci` fails | Delete `node_modules` and retry, or ensure Node 20+ |
97+
| Tests fail to run | Ensure `vitest` is installed: `npm ci` |
98+
| Build fails on types | Run `npx tsc --noEmit` to see TypeScript errors |

docs/screenshot.png

9.41 MB
Loading

eslint.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
},
28+
)

index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/fov-viewer/vite.svg" />
5+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📷</text></svg>" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'" />
8+
<meta name="referrer" content="no-referrer" />
79
<title>FOV Viewer — Focal Length Comparison Tool</title>
810
</head>
911
<body>

0 commit comments

Comments
 (0)