[AI Filter] 11. 5주차 화요일
1. 웹캠 레이아웃 수정

원본 > 일반 AI 이 순서로 레이아웃을 수정하고 두가지 기능을 추가했다.
2. 양쪽 비교 & 확대
사실 단순 이미지 확대기능은 그렇게 어렵지 않은데 양쪽을 비교하는 중간을 확대하는 코드는 조금 복잡하게 느껴졌다.

div태그가 두개 겹쳐있는 걸 작은 Div태그로 옮긴다고 생각하며 코드 구현했다.
이때 이미지 objectPosition 기능을 이용해 확대기능을 구현했다
전체코드
import { MouseEvent } from "react";
import { styled } from "@mui/material/styles";
import { useTheme } from "@material-ui/core/styles";
import { Box, IconButton } from "@mui/material";
import { useState, useRef } from "react";
import beforeImage from "../../Assets/Image/beforeImage.png";
import afterImage from "../../Assets/Image/afterImage.png";
import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
import ArrowRightIcon from "@mui/icons-material/ArrowRight";
const PageDiv = styled("div")({
position: "relative",
height: "100vh",
width: "99.5vw",
background: `url(${afterImage})`,
backgroundSize: "cover",
overflow: "hidden",
});
const Magnify = styled("div")({
width: "300px",
height: "300px",
position: "absolute",
boxShadow: "0 0 0 3px rgba(255, 255, 255, 0.85), 0 0 3px 3px rgba(0, 0, 0, 0.25)",
display: "none",
overflow: "hidden",
zIndex: "2",
});
function HomeBeforeAfterTest() {
const theme = useTheme();
const RATIO = 2;
const magnifyRef = useRef<HTMLDivElement>(null);
const [imgWidth, setImgWidth] = useState<number>(window.innerWidth / 2);
const [objectPosition, setObjectPosition] = useState<string>("-10px 50px");
const buttonStyle = {
position: "absolute",
zIndex: "2",
left: imgWidth,
top: "50%",
transform: "translate(-50%)",
cursor: "move",
};
const beforeTextStyle = {
color: theme.palette.primary.contrastText,
position: "absolute",
top: "50%",
left: window.screen.availWidth / 2 - 100 - 200 * ((window.screen.availWidth - imgWidth) / window.screen.availWidth),
transform: "translate(-50%, -50%)",
};
const AfterTextStyle = {
color: theme.palette.primary.main,
position: "absolute",
top: "50%",
left: window.screen.availWidth / 2 + 100 + 200 * (1 - (window.screen.availWidth - imgWidth) / window.screen.availWidth),
transform: "translate(-50%, -50%)",
};
const CustomTypography = styled("span")({
fontFamily: "Dancing Script",
fontSize: "150px",
});
const handleMove = (event: MouseEvent<HTMLDivElement>) => {
setImgWidth(event.clientX);
setMagnify(event);
};
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
setImgWidth(event.clientX);
};
const setMagnify = (event: MouseEvent<HTMLElement>) => {
let mouseX = event.pageX - event.currentTarget.offsetLeft;
let mouseY = event.pageY - event.currentTarget.offsetTop;
if (!magnifyRef.current?.style) return;
const w = magnifyRef.current.offsetWidth / 2;
const h = magnifyRef.current.offsetHeight / 2;
magnifyRef.current.style.display = "inline-block";
magnifyRef.current.style.left = `${mouseX - w}px`;
magnifyRef.current.style.top = `${mouseY - h}px`;
setObjectPosition(`-${mouseX * RATIO - w}px -${mouseY * RATIO - h}px`);
console.log(objectPosition);
};
const handleMouseLeave = () => {
if (!magnifyRef.current?.style) return;
magnifyRef.current.style.display = "none";
};
return (
<>
<PageDiv onClick={handleClick} onMouseMove={handleMove} onMouseLeave={handleMouseLeave}>
<div
style={{
position: "relative",
zIndex: "1",
width: imgWidth,
height: "100%",
overflow: "hidden",
borderRight: "solid 1px white",
}}
>
<CustomTypography sx={beforeTextStyle}>Before</CustomTypography>
<Box
component="img"
src={beforeImage}
sx={{ width: "99.5vw", height: "100%", objectFit: "cover", objectPosition: "left top" }}
></Box>
</div>
<IconButton sx={buttonStyle}>
<ArrowLeftIcon sx={{ color: "#F2FFFF" }} />
<ArrowRightIcon sx={{ color: "#F2FFFF" }} />
</IconButton>
<CustomTypography sx={AfterTextStyle}>After</CustomTypography>
<Magnify ref={magnifyRef}>
<Box
component="img"
src={afterImage}
sx={{
position: "absolute",
top: "0",
left: "0",
width: "190vw",
height: "100vh",
objectFit: "cover",
objectPosition,
}}
></Box>
<div style={{ position: "relative", width: "150px", borderRight: "solid 1px white", overflow: "hidden" }}>
<Box
component="img"
src={beforeImage}
sx={{
width: "190vw",
height: "200vh",
objectFit: "cover",
objectPosition,
}}
></Box>
</div>
</Magnify>
</PageDiv>
</>
);
}
export default HomeBeforeAfterTest;
확대하는 코드
const setMagnify = (event: MouseEvent<HTMLElement>) => {
let mouseX = event.pageX - event.currentTarget.offsetLeft;
let mouseY = event.pageY - event.currentTarget.offsetTop;
if (!magnifyRef.current?.style) return;
const w = magnifyRef.current.offsetWidth / 2;
const h = magnifyRef.current.offsetHeight / 2;
magnifyRef.current.style.display = "inline-block";
magnifyRef.current.style.left = `${mouseX - w}px`;
magnifyRef.current.style.top = `${mouseY - h}px`;
setObjectPosition(`-${mouseX * RATIO - w}px -${mouseY * RATIO - h}px`);
};
이미지 크기를 확대해놓은 뒤 objectPosition을 현재 상대적인 마우스 위치 * 배율 - 확대경 크기 절반 을 이용해 각각 마이너스 해주었다.
결과화면

3. 양쪽 이미지 확대

이 또한 기본 이미지 확대와는 달랐다. 왜냐하면 일반필터, AI필터 두 영역중에 한군데라도 마우스가 올라가면 두 영역의 동일한 위치가 확대되어 보여야하기 때문이다.
컴포넌트도 구조화되어 있어서 데이터를 어떻게 주고받을까 고민했다.
WebcamFilterExperience.tsx
└ ResultCard.tsx
└ Magnify.tsx
└ ResultCard.tsx
└ Magnify.tsx
위와같은 구조로 짜여져있고, 둘중 하나의 ResultCard 이벤트를 두개의 ResultCard가 모두 감지해 하위 Magnify로 위치정보를 전달해주어야 했다. 따라서 ResultCard의 이벤트를 상위 WebcamFilterExperience에서 감지해 모든 하위 컴포넌트로 데이터를 전송하는 방식으로 문제를 해결했다.
Magnify.tsx
import { styled } from "@mui/material/styles";
import { Box } from "@mui/material";
import { useRef, useState, useEffect } from "react";
type MagnifyProps = {
width: string;
height: string;
RATIO: number;
imgSrc: string | undefined;
pos: {x: number, y: number} | undefined;
};
const MagnifyDiv = styled("div")({
width: "250px",
height: "250px",
position: "absolute",
boxShadow: "0 0 0 3px rgba(255, 255, 255, 0.85), 0 0 3px 3px rgba(0, 0, 0, 0.25)",
display: "none",
overflow: "hidden",
});
function Magnify({ width, height, RATIO, imgSrc, pos }: MagnifyProps) {
const magnifyRef = useRef<HTMLDivElement>(null);
const [objectPosition, setObjectPosition] = useState<string>("");
useEffect(() => {
if (pos) {
setMagnify();
} else {
if (!magnifyRef.current?.style) return;
magnifyRef.current.style.display = "none";
}
}, [pos]);
const setMagnify = () => {
if (!pos) return;
if (!magnifyRef.current?.style) return;
const w = magnifyRef.current.offsetWidth / 2;
const h = magnifyRef.current.offsetHeight / 2;
magnifyRef.current.style.display = "inline-block";
if (!magnifyRef.current.parentElement) return
magnifyRef.current.style.left = `${magnifyRef.current.parentElement?.offsetLeft + pos.x - w}px`;
magnifyRef.current.style.top = `${magnifyRef.current.parentElement?.offsetTop + pos.y- h}px`;
setObjectPosition(`-${pos.x * RATIO - w}px -${pos.y * RATIO - h}px`);
};
return (
<MagnifyDiv ref={magnifyRef}>
<Box
component="img"
src={imgSrc}
sx={{
position: "relative",
top: "0",
left: "0",
width: `${parseInt(width.substring(0, width.length - 2)) * RATIO}px`,
height: `${parseInt(height.substring(0, height.length - 2)) * RATIO}px`,
objectPosition,
}}
></Box>
</MagnifyDiv>
);
}
export default Magnify;
ResultCard.tsx
import { MouseEvent } from "react";
import { styled } from "@mui/material/styles";
import Magnify from "../Commons/Magnify";
type ResultCardProps = {
imgSrc: string | undefined;
title: string;
width: string;
height: string;
setMousePos: Function;
pos: { x: number; y: number } | undefined;
};
const TitleSpan = styled("span")({
color: "#CEF3FF",
fontSize: "1.5rem",
fontWeight: "600",
padding: "5px",
marginBottom: "36px",
});
function ResultCard({ imgSrc, title, width, height, setMousePos, pos }: ResultCardProps) {
const handleMove = (event: MouseEvent<HTMLElement>) => {
if (!imgSrc) return
let mouseX = event.pageX - event.currentTarget.offsetLeft;
let mouseY = event.pageY - event.currentTarget.offsetTop;
if (mouseX <= 0 || mouseX > event.currentTarget.offsetWidth || mouseY <= 0 || mouseY > event.currentTarget.offsetHeight) {
setMousePos(undefined)
return
}
setMousePos({x: mouseX, y: mouseY})
};
return (
<div>
<div style={{ display: "flex", margin: "1.5rem", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
<TitleSpan>{title}</TitleSpan>
<div onMouseMove={handleMove} className="dashed_border d-flex justify-content-center align-items-center" style={{ overflow: "hidden", height, width }}>
{imgSrc ? (
<>
<Magnify pos={pos} imgSrc={imgSrc} RATIO={3} width={width} height={height} />
<img src={imgSrc} style={{ height, width }} alt="img" />
</>
) : (
"웹캠을 켜주세요"
)}
</div>
</div>
</div>
);
}
export default ResultCard;
상위 컴포넌트에서 props로 전달받은 함수 setMousePos를 이용해 onMouseMove 이벤트를 전달한다.
WebcamFilterExperience.tsx
const sendMousePos = (pos: { x: number; y: number } | undefined) => {
if (!showMagnify) return
if (pos) {
setMousePos(pos);
} else {
setMousePos(undefined);
}
};
상위 컴포넌트에서 SetMousePos로 마우스 상대위치 상태관리를 해주고 이 정보는 하위 ResultCard > Magnify로 전달된다. 사실 데이터 흐름이 비효율적인 것 같지만 내 지식으로는 이런 데이터 흐름밖에 생각나지 않았다. 혹시 추후에 다른 효율적인 방법이 떠오른다면 다시 업로드 해야겠다.
마무리
이번 프로젝트도 거의 다 끝나간다. 매일 블로그 쓰기는 비록 지키지 못했지만 프로젝트를 진행하며 발생한 문제상황과 그 문제를 해결한 과정, 고민한 과정을 적을 수 있는 공간이 생겼다는 것만으로 이번 프로젝트 기록하기는 성공한거같다!! 내일 또 다른 기능을 구현할 것 같으니까 내일은 내일의 고민 결과를 적는걸로..
Author And Source
이 문제에 관하여([AI Filter] 11. 5주차 화요일), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@hablobien/AI-Filter-11.-5주차-화요일저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)