Skip to content

Commit b37d4e6

Browse files
committed
Add filters
1 parent 523e66d commit b37d4e6

File tree

9 files changed

+217
-4
lines changed

9 files changed

+217
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
TODO.md
2+
.github/
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.filter-controls {
2+
margin-top: 1rem;
3+
padding: 1rem;
4+
background-color: #f5f5f5;
5+
border-radius: 4px;
6+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
7+
}
8+
9+
.filter-controls h3 {
10+
margin-top: 0;
11+
margin-bottom: 0.5rem;
12+
font-size: 1rem;
13+
color: #333;
14+
}
15+
16+
.filter-options {
17+
display: flex;
18+
flex-direction: column;
19+
gap: 0.5rem;
20+
}
21+
22+
.filter-option {
23+
display: flex;
24+
align-items: center;
25+
}
26+
27+
.filter-option input[type="checkbox"] {
28+
margin-right: 0.5rem;
29+
}
30+
31+
.filter-option label {
32+
cursor: pointer;
33+
font-size: 0.9rem;
34+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import { RootState } from '../store';
4+
import { PhotoFilter } from '../store/photo/types';
5+
import { addFilter, removeFilter } from '../store/photo/actions';
6+
import { AppDispatch } from '../store';
7+
import './FilterControls.css';
8+
9+
const FilterControls: React.FC = () => {
10+
const dispatch = useDispatch<AppDispatch>();
11+
const photo = useSelector((state: RootState) => state.photo.photo);
12+
13+
const handleFilterChange = (filter: PhotoFilter) => {
14+
if (!photo) return;
15+
16+
const isFilterApplied = photo.filters.includes(filter);
17+
18+
if (isFilterApplied) {
19+
dispatch(removeFilter(filter));
20+
} else {
21+
dispatch(addFilter(filter));
22+
}
23+
};
24+
25+
if (!photo) {
26+
return null;
27+
}
28+
29+
return (
30+
<div className="filter-controls">
31+
<h3>Filters</h3>
32+
<div className="filter-options">
33+
<div className="filter-option">
34+
<input
35+
type="checkbox"
36+
id="sepia-filter"
37+
checked={photo.filters.includes(PhotoFilter.SEPIA)}
38+
onChange={() => handleFilterChange(PhotoFilter.SEPIA)}
39+
/>
40+
<label htmlFor="sepia-filter">Sepia</label>
41+
</div>
42+
<div className="filter-option">
43+
<input
44+
type="checkbox"
45+
id="bw-filter"
46+
checked={photo.filters.includes(PhotoFilter.BLACK_AND_WHITE)}
47+
onChange={() => handleFilterChange(PhotoFilter.BLACK_AND_WHITE)}
48+
/>
49+
<label htmlFor="bw-filter">Black & White</label>
50+
</div>
51+
</div>
52+
</div>
53+
);
54+
};
55+
56+
export default FilterControls;

photo-editor/src/components/ImageRenderer.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
11
import React, { useRef, useEffect } from 'react';
22
import { useSelector } from 'react-redux';
33
import { RootState } from '../store';
4+
import { PhotoFilter } from '../store/photo/types';
45
import './ImageRenderer.css';
56

7+
const applyFilter = (imageData: ImageData, filter: PhotoFilter): void => {
8+
const data = imageData.data;
9+
10+
for (let i = 0; i < data.length; i += 4) {
11+
const r = data[i];
12+
const g = data[i + 1];
13+
const b = data[i + 2];
14+
15+
switch (filter) {
16+
case PhotoFilter.SEPIA:
17+
data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189));
18+
data[i + 1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168));
19+
data[i + 2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131));
20+
break;
21+
case PhotoFilter.BLACK_AND_WHITE:
22+
const avg = (r + g + b) / 3;
23+
data[i] = avg;
24+
data[i + 1] = avg;
25+
data[i + 2] = avg;
26+
break;
27+
}
28+
}
29+
};
30+
631
const ImageRenderer: React.FC = () => {
732
const canvasRef = useRef<HTMLCanvasElement>(null);
833

@@ -41,6 +66,23 @@ const ImageRenderer: React.FC = () => {
4166
const scaleY = photo.dimensions.height / img.height;
4267
ctx.scale(scaleX, scaleY);
4368
ctx.drawImage(img, -img.width / 2, -img.height / 2, img.width, img.height);
69+
70+
// Apply filters if present
71+
if (photo.filters && photo.filters.length > 0) {
72+
const imageData = ctx.getImageData(
73+
-img.width / 2,
74+
-img.height / 2,
75+
img.width,
76+
img.height
77+
);
78+
79+
photo.filters.forEach(filter => {
80+
applyFilter(imageData, filter);
81+
});
82+
83+
ctx.putImageData(imageData, -img.width / 2, -img.height / 2);
84+
}
85+
4486
ctx.restore();
4587
};
4688
}, [photo]);

photo-editor/src/components/PhotoControls.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
33
import { resetRotation, rotatePhoto, panZoomPhoto } from '../store/photo/actions';
44
import { selectPhoto } from '../store/photo/selectors';
55
import { AppDispatch } from '../store';
6+
import FilterControls from './FilterControls';
67
import './PhotoControls.css';
78

89
const PhotoControls: React.FC = () => {
@@ -93,6 +94,8 @@ const PhotoControls: React.FC = () => {
9394
<p>Dimensions: {Math.round(photo.dimensions.width)} x {Math.round(photo.dimensions.height)}</p>
9495
</div>
9596
)}
97+
98+
<FilterControls />
9699
</div>
97100
);
98101
};

photo-editor/src/components/PhotoUploader.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ const PhotoUploader: React.FC = () => {
3030
height: img.height
3131
},
3232
source: fileUrl,
33-
rotation: 0
33+
rotation: 0,
34+
filters: []
3435
};
3536

3637
dispatch(setPhoto(photo));

photo-editor/src/store/photo/actions.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import {
33
ROTATE_PHOTO,
44
PAN_ZOOM_PHOTO,
55
SET_PHOTO,
6+
ADD_FILTER,
7+
REMOVE_FILTER,
68
Photo,
9+
PhotoFilter,
710
ResetRotationAction,
811
RotatePhotoAction,
912
PanZoomPhotoAction,
10-
SetPhotoAction
13+
SetPhotoAction,
14+
AddFilterAction,
15+
RemoveFilterAction
1116
} from './types';
1217

1318
// Action creators
@@ -36,3 +41,17 @@ export const setPhoto = (photo: Photo): SetPhotoAction => ({
3641
type: SET_PHOTO,
3742
payload: photo
3843
});
44+
45+
export const addFilter = (filter: PhotoFilter): AddFilterAction => ({
46+
type: ADD_FILTER,
47+
payload: {
48+
filter
49+
}
50+
});
51+
52+
export const removeFilter = (filter: PhotoFilter): RemoveFilterAction => ({
53+
type: REMOVE_FILTER,
54+
payload: {
55+
filter
56+
}
57+
});

photo-editor/src/store/photo/reducer.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
RESET_ROTATION,
55
ROTATE_PHOTO,
66
PAN_ZOOM_PHOTO,
7-
SET_PHOTO
7+
SET_PHOTO,
8+
ADD_FILTER,
9+
REMOVE_FILTER
810
} from './types';
911

1012
// Initial state
@@ -54,6 +56,29 @@ const photoReducer = (
5456
}
5557
};
5658

59+
case ADD_FILTER:
60+
if (!state.photo) return state;
61+
if (state.photo.filters.includes(action.payload.filter)) {
62+
return state;
63+
}
64+
return {
65+
...state,
66+
photo: {
67+
...state.photo,
68+
filters: [...state.photo.filters, action.payload.filter]
69+
}
70+
};
71+
72+
case REMOVE_FILTER:
73+
if (!state.photo) return state;
74+
return {
75+
...state,
76+
photo: {
77+
...state.photo,
78+
filters: state.photo.filters.filter(filter => filter !== action.payload.filter)
79+
}
80+
};
81+
5782
default:
5883
return state;
5984
}

photo-editor/src/store/photo/types.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
// Photo model types
22

3+
// Photo filter enum
4+
export enum PhotoFilter {
5+
SEPIA = 'sepia',
6+
BLACK_AND_WHITE = 'black_and_white'
7+
}
8+
39
// Photo model interface
410
export interface Photo {
511
dimensions: {
@@ -8,6 +14,7 @@ export interface Photo {
814
};
915
source: string; // Relative path to the image
1016
rotation: number;
17+
filters: PhotoFilter[]; // Array of filters to apply to the photo
1118
}
1219

1320
// Initial state interface
@@ -20,17 +27,21 @@ export const RESET_ROTATION = 'photo/RESET_ROTATION';
2027
export const ROTATE_PHOTO = 'photo/ROTATE_PHOTO';
2128
export const PAN_ZOOM_PHOTO = 'photo/PAN_ZOOM_PHOTO';
2229
export const SET_PHOTO = 'photo/SET_PHOTO';
30+
export const ADD_FILTER = 'photo/ADD_FILTER';
31+
export const REMOVE_FILTER = 'photo/REMOVE_FILTER';
2332

2433
// Action interfaces
2534
export interface ResetRotationAction {
2635
type: typeof RESET_ROTATION;
36+
[key: string]: any;
2737
}
2838

2939
export interface RotatePhotoAction {
3040
type: typeof ROTATE_PHOTO;
3141
payload: {
3242
rotation: number;
3343
};
44+
[key: string]: any;
3445
}
3546

3647
export interface PanZoomPhotoAction {
@@ -41,16 +52,36 @@ export interface PanZoomPhotoAction {
4152
height: number;
4253
};
4354
};
55+
[key: string]: any;
4456
}
4557

4658
export interface SetPhotoAction {
4759
type: typeof SET_PHOTO;
4860
payload: Photo;
61+
[key: string]: any;
62+
}
63+
64+
export interface AddFilterAction {
65+
type: typeof ADD_FILTER;
66+
payload: {
67+
filter: PhotoFilter;
68+
};
69+
[key: string]: any;
70+
}
71+
72+
export interface RemoveFilterAction {
73+
type: typeof REMOVE_FILTER;
74+
payload: {
75+
filter: PhotoFilter;
76+
};
77+
[key: string]: any;
4978
}
5079

5180
// Union of all photo action types
5281
export type PhotoActionTypes =
5382
| ResetRotationAction
5483
| RotatePhotoAction
5584
| PanZoomPhotoAction
56-
| SetPhotoAction;
85+
| SetPhotoAction
86+
| AddFilterAction
87+
| RemoveFilterAction;

0 commit comments

Comments
 (0)