밝기 조절하기
- 이미지의 RGB 값을 밝기 계수(factor)만큼 곱해서 전체 밝기를 조절하는 함수
lib.rs
#[wasm_bindgen]
pub fn brightness(data: &mut [u8], value: u32) {
let factor = value as f32 / 100.0;
for i in (0..data.len()).step_by(4) {
let r = data[i] as f32 * factor;
let g = data[i + 1] as f32 * factor;
let b = data[i + 2] as f32 * factor;
data[i] = r.min(255.0).max(0.0) as u8;
data[i + 1] = g.min(255.0).max(0.0) as u8;
data[i + 2] = b.min(255.0).max(0.0) as u8;
}
}
#[wasm_bindgen]
- WASM으로 export되며 JS에서 호출할 수 있게 됨
입력: data: &mut [u8]
- 1픽셀당 4byte(R,G,B,A)로 이루어진 이미지 전체의 raw pixel 데이터
입력: value: u32
- UI에서 받은 밝기 값(0 ~ 200)
- 100이면 원래 밝기 그대로
- 0으로 갈수록 밝기 감소
- 200으로 갈수록 밝기 증가
let factor = value as f32 / 100.0;
- value = 100 이면 factor = 1.0 으로 변화 없음
- 200 이면 factor = 2.0으로 두 배 밝게
- 50이면 factor = 0.5로 절반 밝기
for i in (0..data.len()).step_by(4) {
- RGBA를 steo_by(4)로 한 픽셀씩 순회
let r = data[i] as f32 * factor;
let g = data[i + 1] as f32 * factor;
let b = data[i + 2] as f32 * factor;
- 각 색 값(0~255)에 factor를 곱해서 밝게/어둡게 만듦
data[i+3]은 건드리지 않는 이유
- 알파는 투명도이기 때문에 밝기와 무관해서
data[i] = r.min(255.0).max(0.0) as u8;
data[i + 1] = g.min(255.0).max(0.0) as u8;
data[i + 2] = b.min(255.0).max(0.0) as u8;
- RGS는 0~255 사이여야 함
- 255을 넘어도 255 고정, 0보다 작으면 0으로 고정
이미지의 모든 픽셀을 순회하면서
RGB 값을 value / 100.0 만큼 곱해서
색 값이 0~255 범위를 넘지 않도록 조절하고
다시 data 배열에 덮어쓰는 방식으로
전체 이미지 밝기를 조절한다.
useFilterBrightness.ts
- 밝기 필터를 적용하는 커스텀 훅
- 캔버스에서 이미지 데이터를 가져오고
- WASM 모듈의 brightness 함수로 밝기를 조절하고
- 수정된 이미지를 다시 캔버스에 반영함
- 만약 이전에 저장해둔 originalPixels(원본 데이터)이 있다면 밝기 조절 시마다 원본에서 다시 시작하도록 만듦
export default function useFilterBrightness() {
const { prepareFilter } = useFilterBase();
const applyBrightness = (
wasm: WasmModule | null,
getCanvasImageData: GetCanvasImageData,
newValue: number,
originalPixels: Uint8ClampedArray<ArrayBuffer> | null,
) => {
const info = prepareFilter(wasm, getCanvasImageData);
if (!info) return;
const { ctx, imageData } = info;
if (originalPixels) imageData.data.set(originalPixels);
wasm?.brightness(imageData.data, newValue);
ctx.putImageData(imageData, 0, 0);
};
return { applyBrightness };
}
const { prepareFilter } = useFilterBase();
- 필터를 적용하기 위한 기본 과정 수행
applyBirghtness(wasm, getCanvasImageData, newValue, originalPixels)
- wasm: WASM 모듈 (필터 연산 수행)
- getCanvasImageData: 캔버스의 현재 imageData를 가져옴
- newValue: 밝기 강도
- originalPixels: 원본 이미지의 RGBA 배열
if (originalPixels) imageData.data.set(originalPixels);
- 밝기가 누적되지 않게 하는 역할
- 사용자가 밝기를 조절하다가 값을 다시 올리거나 내릴 때
- 원본 기반에서 다시 계산함
wasm?.brightness(imageData.data, newValue);
- WASM으로 밝기 조정 알고리즘 수행
- imageData.data: Unit8ClampedArray(RGBA 데이터)
- newValue: 밝기 조절 값
- WASM 내부에서 빠르게 연산됨
ctx.puImageData(imageData, 0, 0);
- 변경된 픽셀 데이터를 canvas에 다시 그림
흑백과 밝기가 서로 상태를 유지하면서 동시에 적용되게 만들기
- 밝기 150 → 흑백 적용 → 밝기 150 유지
- 흑백 ON 상태에서 밝기 80으로 변경 → 흑백 유지
- 흑백 OFF → 밝기만 적용된 화면 유지
- 밝기 OFF + 흑백 ON 조합도 OK
- 다시 흑백 ON/OFF 반복해도 오류 없음
- 모든 필터가 항상 원본 이미지 데이터(originalPixels) 기준으로 정확하게 작동
필터 누적 구조로 바꿔야함
핵심 원리
- Canvas가 필터 적용된 상태를 저장하는게 아닌
- 항상 원본 -> 밝기 적용 -> 흑백 적용 순서로 다시 렌더링해야 함
- 그래야 밝기와 흑백이 서로 영향을 주지 않고
- 원본이 절대 변하지 않음
항상 originalPixels -> 현재 필터 상태(brightness, grayscale) 조합으로 재계산
필터 누적 방식
function applyFilters() {
if (!originalPixels) return;
// 1. 원본 픽셀을 canvas에 복구
resetCanvasTo(originalPixels);
// 2. 밝기 적용
if (brightness !== 100) {
applyBrightness(wasm, getCanvasImageData, brightness, originalPixels);
}
// 3. 흑백 적용
if (isGrayscale) {
applyGrayscale(wasm, getCanvasImageData);
}
}
- canvas는 상태 저장소가 아니라 render target(출력)이 되는 것
- 필터를 바꾸면 canvas가 초기화 되고 필터 조합이 다시 적용됨
BrightnessComponent.tsx와 GrayScaleComponent.tsx는 값만 바꾸는 역할
page/edit.tsx에서 canvas를 설정시키는 역할
BrightnessComponent.tsx
export default function BrightnessComponent({ disabled, brightness, setBrightness }: BrightnessComponentProps) {
const handleBrightness = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value);
setBrightness(newValue);
};
GrayScaleComponent.tsx
export default function GrayScaleComponent({disabled, isGray, setIsGray}: GrayScaleComponentProps) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsGray(e.target.checked);
};
- 두 컴포넌트 다 값을 설정하는 역할만 수행함
page/editor.tsx
- 밝기 + 흑백 필터를 항상 원본을 기준으로 다시 계산해서 canvas에 그리는 구조
- canvas 상태가 필터 누적 결과가 아닌 최신 필터 조합임
useEffect(() => {
setBrightness(100);
setIsGray(false);
}, [image]);
const applyAllFilters = useCallback(() => {
if (!wasm || !image || !originalPixels) return;
resetColor(getCanvasImageData, originalPixels);
if (brightness !== 100) applyBrightness(wasm, getCanvasImageData, brightness, originalPixels);
if (isGray) applyGrayscale(wasm, getCanvasImageData);
}, [wasm, image, originalPixels, brightness, isGray, getCanvasImageData, resetColor, applyBrightness, applyGrayscale]);
useEffect(() => {
applyAllFilters();
}, [brightness, isGray, applyAllFilters]);
useCallback(() => {...}, [...]); 이란?
- 함수를 메모이제이션하는 훅
- 함수가 매번 새로 만들어지는 것을 막고, 의존성이 바뀔 때만 새로 생성되게 함
필요한 이유?
1. React 컴포넌트는 렌더링될 때마다 내부의 함수들을 다시 생성
function MyComponent() {
const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Click</button>;
}
- handleClick은 리렌더링될 때마다 새로운 함수로 다시 만듦
- 자식 컴포넌트에 props로 넘기는 함수면 문제 발생 가능
2. useCallback은 함수를 기억해둠
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
- [] 의존성이 변하지 않으면
- handleClick은 항상 같은 함수 인스턴스를 재사용함
3. 자식 컴포넌트가 React.memo()로 최적화돼 있을 때 필요
- 부모에서 전달하는 함수가 매번 새로 만들어지는 함수면 React.memo가 있어도 자식이 계속 재렌더링되어 성능 낭비
const applyAllFilters = useCallback(() => {
...
}, [brightness, isGray]);
- brightness, isGray 값이 바뀔 때만 새로 다시 함수가 생성됨
- 다른 이유로 부모가 리렌더링되어도 applyAllFilters는 동일한 함수로 유지됨
여기서 필요한 이유
- 만약 useCallback이 없다면 editor 페이지가 리렌더링될 때마다 매번 새로운 함수가 생성됨
- useEffect에서 의존성 비교 시
- applyAllFilters가 매번 달라짐(참조 불일치)
- useEffect가 매번 실행되어 필터 재적용
- 불필요한 재렌더링 + 성능 저하 + fickering 발생 가능
- useEffect: 값이 바뀌면 실행
- useCallback: 함수를 기억해. 값이 바뀌면 새로 만들어
useEffect(() => {
setBrightness(100);
setIsGray(false);
}, [image]);
- 새로운 이미지가 업로드되거나 이미지가 바뀔 때
- 밝음을 100, 흑백을 off로 설정
- 새 이미지가 들어올 때 필터 상태를 클린하게 리셋
const applyAllFilters = useCallback(() => {
if (!wasm || !image || !originalPixels) return;
resetColor(getCanvasImageData, originalPixels);
if (brightness !== 100)
applyBrightness(wasm, getCanvasImageData, brightness, originalPixels);
if (isGray)
applyGrayscale(wasm, getCanvasImageData);
}, [wasm, image, originalPixels, brightness, isGray, getCanvasImageData, resetColor, applyBrightness, applyGrayscale]);
- 항상 다음 순서로 캔버스를 다시 그림
1. 캔버스를 원본 이미지로 리셋
- 필터가 누적되지 않도록 canvas를 항상 originalPixels에서 시작
2. 밝기 적용
- brightness가 100이면 원본 밝기 그대로
- 100보다 크거나 작으면 그 값만큼 필터 적용
3. 흑백 적용
- grayscale 체크되어 있으면 흑백 필터를 마지막에 적용
흑백 말고 밝기를 먼저 설정하는 이유
1. 밝기는 R/G/B 개별 채널에 적용되고 흑백은 최종 RGB 값을 한 값으로 합쳐버리기 때문
// 밝기
// 각 픽셀의 R, G, B 채널 각각에 변화를 줌
(120, 80, 200) → brightness 150 → (180, 130, 255)
// 흑백
// 픽섹 최종 RGB를 하나의 값으로 압축함
(180, 130, 255) → gray → (190, 190, 190)
만약 흑백 -> 밝기 순서로 하면?
// 먼저 흑백 적용
(120, 80, 200) → grayscale → (133, 133, 133)
// 나중에 밝기 적용
(133, 133, 133) → brightness 150 → (200, 200, 200)
- 원본 정보(RGB 비율)를 이미 잃었기 때문에 밝기 조절이 회색 덩어리의 밝기만 조절하는 것이 됨
- 색 정보가 손실된 상태에서 밝기를 조절함
최적의 필터 파이프라인 순서
1) Reset (원본으로 리셋)
2) Brightness / Exposure / Gamma
3) Contrast / Shadows / Highlights
4) Saturation / Hue / Color Balance
5) Grayscale / Sepia / Duotone
6) Blur / Sharpen / Edge
7) Vignettes / LUT / Noise / Effects
- 빛 -> 톤 -> 색 -> 흑백 / 세피아 -> 공간 -> 특효
- canvas 상태를 사용하지 않고 항상 originalPixels에서 다시 계산하므로 밝기, 흑백을 아무리 반복해도 깨지지 않음
- 필터 상태를 서로 조합할 수 있음
useEffect(() => {
applyAllFilters();
}, [brightness, isGray, applyAllFilters]);
- 밝기, 흑백이 바뀔 때마다 canvas를 다시 그려줘야 하므로 applyAllFilters() 실행함
setFilter: 필터 상태에서 특정 한 항목만 바꾸는 함수
const [filters, setFilters] = useState({
brightness: 100,
isGray: false,
});
const { brightness, isGray } = filters;
const setFilter = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
}
- filters 객체 안에서 특정 key(brightness, isGray 등)만 업데이트하는 함수
- filters는 이런 객체
filters = {
brightness: number,
isGray: boolean
}
key: keyof typeof filters
- key는 두 개 중 하나의 string만 받을 수 있음
- setFilter("brightness", 120); setFilter("isGray", true);
- filters 안에 존재해야지만 가능함
setFilters(prev => ({ ...prev, [key]: value });
- 기존 filters 객체(prev)를 복사하고
- 특정 key만 새 value로 덮어쓰기
setFilters(prev => ({
brightness: prev.brightness,
isGray: prev.isGray,
[key]: value
}));
'Rust' 카테고리의 다른 글
| [트러블슈팅]next.js16과 tailwindcss v4 반응형 웹 오류 (3) | 2025.11.22 |
|---|---|
| 트러블 슈팅 (1) | 2025.11.21 |
| 이미지 다운로드하기 (0) | 2025.11.20 |
| 대비 만들기 (0) | 2025.11.18 |
| 파일 업로드 & 흑백 필터 적용 (0) | 2025.11.14 |