[AI Filter] 11. 5주차 화요일

66405 단어 AI FilterAI Filter

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로 전달된다. 사실 데이터 흐름이 비효율적인 것 같지만 내 지식으로는 이런 데이터 흐름밖에 생각나지 않았다. 혹시 추후에 다른 효율적인 방법이 떠오른다면 다시 업로드 해야겠다.

마무리

이번 프로젝트도 거의 다 끝나간다. 매일 블로그 쓰기는 비록 지키지 못했지만 프로젝트를 진행하며 발생한 문제상황과 그 문제를 해결한 과정, 고민한 과정을 적을 수 있는 공간이 생겼다는 것만으로 이번 프로젝트 기록하기는 성공한거같다!! 내일 또 다른 기능을 구현할 것 같으니까 내일은 내일의 고민 결과를 적는걸로..

좋은 웹페이지 즐겨찾기