.Zzumbong

[DEV] 프론트엔드 개발자 포트폴리오 Intro 리뷰 본문

Dev

[DEV] 프론트엔드 개발자 포트폴리오 Intro 리뷰

쭘봉 2024. 3. 20. 12:26

항상 탐험하듯이 개발하고 싶어 이런 슬로건을 걸었다.

 

keep calm and develop! 이라는 flag를 가진 우주인은 나를 캐릭터화 한 그림이다.
Astronauts 그림을 그려준 전 여자친구 현 와이프에게 압도적 감사!

 

 

전전직장에 입사하기 이전인 5년 전에 만들었던 포폴이 있다.

사실 지금도 사용중이지만 간단한 내용 업데이트만 해놓은 상태이다.

react 16버전으로 만들었고 class component를 사용 할 정도로 오래된 프로젝트이다.

 

[포폴 사이트 구경가기]

 

 

 

오늘 리뷰할 부분은 인트로 부분이다.

 

 

5년전에 만들어서 다른 부분은 약간 촌스러워도

영혼을 갈아 넣었던 인트로 부분은 아직도 기가막히다.

 

 

이 부분은 총 6장의 캔버스로 이루어져 있다. 

뒷 배경에서 은하수를 표현하는 milky가 1장, earth 컨테이너가 가진 4장, loaded가 1장이다.

earth는 총 3개의 캔버스에서 이미지와 마스킹으로 표현하고

shading으로 좀 더 현실 같은 지구를 표현하기 위해, 반사, 그림자, 대기권을 표현한다.

loading 캔버스는 컴포넌트 로드 시 검은 배경에서 서서히 나온 후 약간의 어두운 마스킹을 위해 추가했다.

 

 

지구를 구성하는 이미지는 총 6장인데,

밤 부분 이미지
지구의 구름

 

지구 대륙

 

밤과 낮 로딩 시 쓰이는 전체부분 마스킹 이미지로 구성되어있다.

지구이미지는 전부 4k이미지고 몇 시간의 구글링 끝에 다운받을 수 있었다.

3D모델에서 사용하는 텍스쳐 이미지에서 겨우 건저냈다.

마스킹 이미지는 포토샵으로 직접 그렸는데,

이 3장 이미지 때문에 포토샵을 구매할 수 없어서 온라인 무료 이미지 편집사이트에서 만듦..

 

컴포넌트가 로딩되면 

  componentDidMount() {
    this.setupCanvas();
    window.addEventListener('scroll', this.onScroll);
  }

캔버스 셋업 메소드와 스크롤 이벤트를 호출한다.

milkyearthparallax scroll로 조금씩 상단으로 올라가기 때문에 스크롤 이벤트도 필수.

 

 

처음 들어가서

대륙만 있는 이미지를 로딩하면

이렇게 보인다. ㅋㅋ

 

그후 마스킹된 밤 부분을 로드해서 위에 올려준다.

 

 

이렇게 로드하면 대륙에 낮인 부분에서 

 

자연스럽게!!

 

여튼 이런식으로 낮에서 밤으로 넘어갈때 실제처럼 불빛이 켜지는 것처럼 보이게 할 수 있다.

 

 

 

이제 구름을 넣으면 50%는 완성이다.

 

이제 셰이딩 부분을 적용 해주면 된다. 

이 부분은 아주 평면적인 지구 2D 이미지를 3D처럼 보이게하는 착시 효과를 주기위해 개발했다.

 

  drawShading() {
    let CTX_SHADING = this.SPACE.shading;
    let CTX_LOADING2 = this.SPACE.loading2;
    let CENTER_X = this.getX('center');
    let CENTER_Y = this.getY('center');
    let EARTH_SIZE = this.state.earth_height / 2;

    // 외곽 glow
    this.drawArc(CTX_SHADING, CENTER_X, CENTER_Y, EARTH_SIZE + 150);
    this.drawRGradient(
      CTX_SHADING,
      {
        x: CENTER_X,
        y: CENTER_Y,
        size: EARTH_SIZE,
      },
      {
        x: CENTER_X,
        y: CENTER_Y,
        size: EARTH_SIZE + 55,
      },
      [
        [0, 'transparent'],
        [0.3, 'rgba(33, 84, 150, 0.8)'],
        [1, 'transparent'],
      ]
    );

    // 내부 glow
    this.drawArc(CTX_SHADING, CENTER_X, CENTER_Y, EARTH_SIZE);
    this.drawRGradient(
      CTX_SHADING,
      {
        x: CENTER_X,
        y: CENTER_Y,
        size: EARTH_SIZE - 100,
      },
      {
        x: CENTER_X,
        y: CENTER_Y,
        size: EARTH_SIZE,
      },
      [
        [0, 'transparent'],
        [0.0001, 'rgba(60, 105, 175, 0.01)'],
        [1, 'rgba(60, 105, 175, 0.4)'],
      ]
    );

    // 태양 빛
    this.drawArc(CTX_SHADING, CENTER_X, CENTER_Y, EARTH_SIZE);
    this.drawRGradient(
      CTX_SHADING,
      {
        x: CENTER_X + CENTER_X / 2,
        y: CENTER_Y - CENTER_Y / 2,
        size: 0,
      },
      {
        x: CENTER_X,
        y: CENTER_Y,
        size: EARTH_SIZE,
      },
      [
        [0, 'rgba(255, 255, 255, 0.5)'],
        [1, 'transparent'],
      ]
    );

    this.drawArc(CTX_LOADING2, CENTER_X, CENTER_Y, EARTH_SIZE + 10);
    this.drawRGradient(
      CTX_LOADING2,
      {
        x: CENTER_X + CENTER_X / 2,
        y: CENTER_Y - CENTER_Y / 2,
        size: 0,
      },
      {
        x: CENTER_X,
        y: CENTER_Y,
        size: EARTH_SIZE,
      },
      [
        [0, 'rgba(0,0,0,0.85)'],
        [0.5, 'rgba(0,0,0,0.95)'],
        [1, 'rgba(0,0,0,1)'],
      ]
    );
  }

 

 

코드로 만들어진 셰이딩을 마지막에 로드한다. 유무 차이

 

지금보니 외부 glow부분이 너무 밝긴하다. 위 사진처럼 연하게 했다면 더 좋았을 뜻.

 

 

로드 후 마지막으로 animate를 실행 시켜 로드한다.

 

 

setupCanvas부분을 보면 이해하기 쉽다.

  setupCanvas() {
    console.log('---setupCanvas---');
    this.SPACE.ctx_B = this.refs.earth_B.getContext('2d');
    this.SPACE.ctx_L = this.refs.earth_L.getContext('2d');
    this.SPACE.ctx_C = this.refs.earth_C.getContext('2d');
    this.SPACE.ctx_back = this.refs.milky.getContext('2d');
    this.SPACE.shading = this.refs.shading.getContext('2d');
    this.SPACE.loading2 = this.refs.loading2.getContext('2d');

    this.drawImageCanvas(
      'b_space',
      this.resource.milkyway,
      this.SPACE.ctx_back,
      0,
      0,
      this.state.canvas_width,
      this.state.canvas_width * 2
    )
      .then(() => {
        return this.drawImageCanvasWithMask(
          'b_light',
          this.resource.earth_L,
          this.SPACE.ctx_L,
          this.getX(),
          this.getY()
        );
      })
      .then(() => {
        return this.drawImageCanvasWithMask(
          'b_black',
          this.resource.earth_B,
          this.SPACE.ctx_B,
          this.getX(),
          this.getY()
        );
      })
      .then(() => {
        return this.drawImageCanvasWithMask(
          'b_cloud',
          this.resource.earth_C,
          this.SPACE.ctx_C,
          this.getX(),
          this.getY()
        );
      })
      .then(() => {
        return this.drawShading();
      })
      .then(() => {
        console.log('로딩 끝');
        this.loadHandler(true);
        setTimeout(() => {
          console.log('로딩 애니메이션 시작');
          this.startAnimate();
        }, 500);
      });
  }

 

 

drawImageCanvas, drawImageCanvasWithMask 함수는 직접 비동기로 구현.

  drawImageCanvas(imgName, imgSrc, ctx, x = 0, y = 0) {
    var loadImg = new Promise((resolve, reject) => {
      this.SPACE[imgName] = {};
      let newImg = new Image();
      newImg.src = imgSrc;
      newImg.onload = () => {
        let rato = this.state.canvas_width / newImg.naturalWidth;
        let height = this.state.canvas_height * rato;

        ctx.drawImage(newImg, x, y, this.state.canvas_width, height);
        this.SPACE[imgName].img = newImg;
        resolve();
      };
    });
    return loadImg;
  }
  drawImageCanvasWithMask(imgName, resource, ctx, x = 0, y = 0) {
    var loadImg = new Promise((resolve, reject) => {
      this.SPACE[imgName] = {};
      let mask = new Image();
      let newImg = new Image();

      mask.src = resource[0];
      newImg.src = resource[1];

      mask.onload = () => {
        ctx.drawImage(
          mask,
          x,
          y,
          this.state.earth_width,
          this.state.earth_height
        );
        ctx.globalCompositeOperation = 'source-in';
        this.SPACE[imgName].mask = mask;
      };
      newImg.onload = () => {
        ctx.drawImage(
          newImg,
          x,
          0,
          this.state.earth_ground,
          this.state.earth_height
        );
        this.SPACE[imgName].img = newImg;
        resolve();
      };
    });
    return loadImg;
  }

 

 

 

 

animate로 애니메이션을 한땀 한땀 구현.

  animate() {
    let X = this.state.x;
    this.SPACE.ctx_L.drawImage(
      this.SPACE.b_light.img,
      X.L,
      this.getY('LAND'),
      this.state.earth_ground,
      this.state.earth_height
    );
    this.SPACE.ctx_B.drawImage(
      this.SPACE.b_black.img,
      X.B,
      this.getY('LAND'),
      this.state.earth_ground,
      this.state.earth_height
    );

    this.SPACE.ctx_C.globalCompositeOperation = 'source-over';
    this.SPACE.ctx_C.clearRect(
      0,
      0,
      this.state.canvas_width,
      this.state.canvas_height
    );
    this.SPACE.ctx_C.drawImage(
      this.SPACE.b_cloud.mask,
      this.getX(),
      this.getY(),
      this.state.earth_width,
      this.state.earth_height
    );
    this.SPACE.ctx_C.globalCompositeOperation = 'source-in';
    this.SPACE.ctx_C.drawImage(
      this.SPACE.b_cloud.img,
      X.C,
      this.getY('LAND'),
      this.state.earth_ground,
      this.state.earth_height
    );

    for (const key in X) {
      if (X[key] < -(this.state.earth_ground / 2)) {
        X[key] = 0;
      }
    }
    this.setState({
      x: {
        L: X.L - 0.05,
        B: X.B - 0.05,
        C: X.C - 0.1,
      },
    });
    requestAnimationFrame(this.animate);
  }

 

  getX(posi) {
    if (posi === 'center') return this.state.canvas_width / 2;
    return (this.state.canvas_width - this.state.earth_width) / 2;
  }
  getY(posi) {
    if (posi === 'center')
      return -(this.state.earth_height / 3) + this.state.earth_height / 2;
    else if (posi === 'LAND') return -(this.state.earth_height / 4);

    return -(this.state.earth_height / 3);
  }

 

 

 

 

 

완성된 지구와 우리 은하수

 

오랜만에 class component를 보니 답답함도 느껴지고 나의 부족함도 느껴지는 코드가 보이기도 하고

나중에 다른 포폴 사이트를 만든다면 없어질 수 있는 사이트지만

 

5년전에 정말 좋은 곳으로 이직하고 싶은 마음과

항상 좋아하는 우주에 관련된 무언가를 만든다는데 열정이 생겨 정말 열심히 만들었다.

 

100% 이것 덕분에 취업이 된건 아니겠지만 나중에 물어보니 포폴이 있어서 더 집중해서 이력서를 봤다고 하더라

 

짧은 시간내에 기획/개발/디자인/로고 모두 고민해보면서 만드는 것에 대한 재미를 깨닫게 된 계기이다.

그래서 어느날 힘들 때, 이 포트폴리오를 보면 과거의 내가 대견스럽기도 해서 다시 힘이 나기도 한다.

 

 

 

 

전체 코드와 개발과정을 readMe에 짧게 써놨다.

아래에서 보도록!

https://github.com/blowROCK/portfolio

 

GitHub - blowROCK/portfolio: 퇴사기념 포트폴리오 시작

퇴사기념 포트폴리오 시작. Contribute to blowROCK/portfolio development by creating an account on GitHub.

github.com

 

Comments