[fabric.js] 내 사진에 직접 인스타 필터를 적용해보자!

Ell!·2022년 4월 30일
0

javascript

목록 보기
6/6

목차

  1. 인스타그램 필터를 만들어보자
  2. 이미지 편집기 만들기
  3. 필터 적용하기
  4. pixel 조정하기
  5. custom filter

인스타그램 필터

이미지 편집기를 만들 던 중, 인스타그램처럼 필터 기능을 제공하고 싶어졌다.

인스타그램에서 다양한 필터를 사진에 적용할 수 있다.

이미지 편집기

우선, 이미지 편집기의 기능을 모두 직접 만들어내자니 시간이 너무 오래걸릴 것 같아서, 기존에 제공하는 이미지 편집기를 개조해서 만들어내기로 했다.

여러 후보가 있었지만 내가 고른 곳은 tui-image-editor. github를 쭉 읽어보면서 왜 이렇게 익숙하지?? 했는데 nhn이었다ㅋㅋ

API 문서를 읽어보면 알겠지만, 여러 쓸만한 기능을 제공하고 있다. 뭔가 살짝씩 애매하긴 하지만 이만큼 친절하고 많은 유저들이 쓰는 라이브러리가 없어서 고르게 되었다.

우선, 나는 react를 기반으로 사용하고 있기 때문에, 적당히 코드를 맞춰서 써야한다. (react에서도 사용할 수 있게 기능을 제공 중이지만, 원하는대로 UI를 짜고 싶어서..)

import ToastImageEditor from 'tui-image-editor';
import 'tui-image-editor/dist/tui-image-editor.css';

const imageEditor = ({editingImage}) => {
 const toastImageEditorRef = useRef(); // image-editor div의 ref
 const editorInstanceRef = useRef(); // 밖에서도 image editor instance 쓰기 위함

  useEffect(() => {
    const options = {
      cssMaxWidth: 400,
      cssMaxHeight: 400,
      selectionStyle: {
        cornerSize: 20,
        rotatingPointOffset: 70,
      },
      usageStatistics: false,
    };

    // instance 정의
    let editorInstance;

    const canvasDiv = toastImageEditorRef.current;
    if (canvasDiv) {
      editorInstance = new ToastImageEditor(
        toastImageEditorRef.current,
        options,
      );
      editorInstanceRef.current = editorInstance; // 밖에서도 쓸 수 있도록
    }

    // 이미지 editor에 불러오기
    if (canvasDiv && editingImage) {
      editorInstance
        .loadImageFromFile(editingImage) // loadImageFromURL이 안되서 file 그자체를 가져와서
        .then(sizeValue => {
          //console.log(sizeValue); // 이미지 넣고 난 후, 이미지의 size
          //console.log(canvasDiv.offsetWidth, canvasDiv.offsetHeight); // canvasDiv의 사이즈
          let maxValue;

          // 가로가 더 긴 사진은 가로 기준, 세로가 더 긴 사진은 세로 기준으로
          if (sizeValue.newWidth > sizeValue.newHeight) {
            maxValue = canvasDiv.offsetWidth;
          } else {
            maxValue = canvasDiv.offsetHeight;
          }

          editorInstance.resizeCanvasDimension({
            width: maxValue,
            height: maxValue,
          });
        })
        .catch(err => console.log(err));
    }
  }, [editingImage]);


}

editorInstanceRef에 new ImageEditor를 넣어주어서 컴포넌트 내에서 사용할 수 있게 해주었다.

ImageEdiotr의 instance를 만들 때, option으로 includeUI를 넣어주면 tui-image-editor가 제공하는 UI가 같이 들어온다. 빼주자.

toastImageEditorRefdiv를 지정해주어야 한다 .(착각해서 canvas를 넣었다가 엄청 헤맸다.)

필터 적용하기

이렇게 인스턴스를 만들어주고 사진을 editor에 load까지 했으니 바로 필터를 적용해보자. 공식문서를 읽어보면 다음과 같이 되있었다.

음 좋아... 그래서 어떤 필터가 있는거죠..? 여기서 1차 멘붕.

그래서 코드를 뒤져봤다. (덕분에 라이브러리 코드 뒤적뒤적하는 스킬은 엄청 늘었다)

ImageEditor class 아래에 _graphics라는 프로퍼티가 있는데, 여기 안에 중요한 것들이 다 모여있었다. 그 아래에 _canvas가 있었는데, 이게 fabric.Canvas의 인스턴스였다.

좀 더 살펴보다가 filter 컴포넌트에 들어가니 applyFilter의 정체(?)를 찾을 수 있었다.

아... 그냥 fabric의 filter를 그대로 쓰는 거구나.. 이제 fabric js로 넘어가야 한다.

pixel 조정하기

그 전에 잠시 인스타 필터는 어떻게 만드는지 알아보자.

다행스럽게도 이전에 이를 구현해서 깃허브에 올리셨던 분이 계셨다. filterous-2

이분의 경우 fabric 같은 라이브러리를 쓰지 않으시고 html5 canvas를 직접 조작하셨는데, 요약하면,

  1. 사진을 canvas에 load
  2. canvas.getContext('2d').getImageData()를 활용해서 image Data 획득
  3. 이 image Data 즉, pixel을 조정해서 필터링.

코드 보고 싶은 사람들을 위한.

이제 이걸 fabric에 녹여내기만 하면 된다.

custom filter

fabric js의 공식 문서를 살펴보자. 여기. 뭔가.. 굉장히 복잡해보인다. 다행히(?) filter class만 만드는 것이 복잡할 뿐. 필터를 적용하는 것은 간단해 보인다.

  const applyCustomFilter = useCallback(e => {
    const filterName = e.target.textContent;

    const editorInstance = editorInstanceRef.current;
    const canvas = editorInstance._graphics._canvas; //fabric Canvas
    const canvasImage = editorInstance._graphics.canvasImage; // fabric Image

    fabric.Image.filters.CustomFilter = createFilterClass(
      'CustomFilter',
      filterName,
    );

    const filter = new fabric.Image.filters.CustomFilter();

    // fabric에서 filter 적용
    canvasImage.filters = [filter];
    canvasImage.applyFilters();
    canvas.renderAll();
  }, []);

custom Filter를 class로 만들어준 후에, 그 인스턴스를 fabric.Image의 filters 프로퍼티에 넣어주고 다시 랜더링시켜주기만 하면 된다.

저기 class를 만드는 boilterplate를 제공해주는 데 es5 문법으로 되어있는지 아주 알아먹기 싫게 생겼다.

정말 다행히도 굳이 webGL을 사용하지 않는다면 이정도만 있어도 충분하다.

import { fabric } from 'fabric';
import imageInstaFilters from 'utils/imageFilter/imageInstaFilters';

export default function createFilterClass(type, filterName) {
  return fabric.util.createClass(fabric.Image.filters.BaseFilter, {
    type: type,
    applyTo2d: options => {
      let imageData = options.imageData;
      let data = imageData.data;
      data = imageInstaFilters[filterName](imageData);
    },
  });
}

imageInstaFilters에는 pixel을 조정하는 함수들이 가득 담겨있다. 하나만 살펴보면

  // Clarendon: adds light to lighter areas and dark to darker areas
  clarendon: pixels => {
    pixels = filters.brightness(pixels, 0.1);
    pixels = filters.contrast(pixels, 0.1);
    pixels = filters.saturation(pixels, 0.15);
    return pixels;
  },
    
    
  // filters
    
    
    brightness: (pixels, adj) => {
      let d = pixels.data;
      adj = adj > 1 ? 1 : adj;
      adj = adj < -1 ? -1 : adj;
      adj = ~~(255 * adj);
      for (let i = 0; i < d.length; i += 4) {
        d[i] += adj;
        d[i + 1] += adj;
        d[i + 2] += adj;
      }
      return pixels;
    },
      
      
      contrast: (pixels, adj) => {
        adj *= 255;
        let d = pixels.data;
        let factor = (259 * (adj + 255)) / (255 * (259 - adj));
        for (let i = 0; i < d.length; i += 4) {
          d[i] = factor * (d[i] - 128) + 128;
          d[i + 1] = factor * (d[i + 1] - 128) + 128;
          d[i + 2] = factor * (d[i + 2] - 128) + 128;
        }
        return pixels;
      },
        
     saturation: (pixels, adj) => {
        let d = pixels.data;
        adj = adj < -1 ? -1 : adj;
        for (let i = 0; i < d.length; i += 4) {
          let r = d[i],
            g = d[i + 1],
            b = d[i + 2];
          let gray = 0.2989 * r + 0.587 * g + 0.114 * b; //weights from CCIR 601 spec
          d[i] = -gray * adj + d[i] * (1 + adj);
          d[i + 1] = -gray * adj + d[i + 1] * (1 + adj);
          d[i + 2] = -gray * adj + d[i + 2] * (1 + adj);
        }
        return pixels;
      },

모두 image data를 직접 조작해주는 함수들이다. 조작한 data를 return해서 class로 만들고, 이를 fabric.Image의 filter에 넣어주는 것이 전부다.

소감

2줄 요약

  • 필터는 image Data의 pixel을 조정해주는 작업이다.
  • fabric Image의 filter에 createClass의 instance를 넣어주면 된다.

이번 기능을 구현하는 것이 상당히 어렵고 힘들었던 기억이 난다. 일단 라이브러리에서 제공하지 않는 기능을 직접 넣어야했고, fabric js의 공식 문서가 아주 불친절...해서... 레퍼런스도 별로 없어서 너무 힘들었다 ㅋㅋ;

덕분에 다른 사람이 쓴 코드를 분석하는 능력을 기를 수 있어서 좋긴했지만 어후~

profile
더 나은 서비스를 고민하는 프론트엔드 개발자.

0개의 댓글