SVG Element에 CSS Animation 적용記

Dzeko·2023년 8월 10일
0

개발일지

목록 보기
97/112
post-thumbnail

PM측에서 편집 중인 타일의 포트에 불이 깜빡거리는 애니메이션이 있으면 좋겠다고 하셨다.

회사에서 쓰는 mxGraph는 캔버스 및 svg로 처리해서 랜더링하기 때문에, html element처럼 애니메이션을 적용하기 어려웠다.
때문에 mxCell 이라는 객체에 스타일을 직접 그려주는 JS 애니메이션을 적용 해야했다.
cell의 색상을 setTimeout과 interval을 주는 방식으로 구현을 했었는데, 잘 되는 듯 했으나 마우스 동작에 의한 사이드 이펙트가 터져 보류하고 빠듯한 개발 일정에 다음 기회에 구현하기로 했었다.

아래가 구현했던 코드 중 일부다.

let flag = true;
let currentFillColor = null;
let currentFontColor = null;

if (run) {
  this.runAnimate = setInterval(() => {
    if (flag) {
      currentFillColor = '#4bc68b';
      currentFontColor = '#ffffff';
    } else {
      currentFillColor = '#ffffff';
      currentFontColor = '#4bc68b';
    }
    animateColors(currentFillColor, currentFontColor, 1, portState);
    flag = !flag;
  }, 1000);
  setTimeout(() => {
    clearInterval(this.runAnimate);
  }, 5000);
} else {
  if (this.runAnimate) {
    clearInterval(this.runAnimate);
  }
  resetPortColor();
}

function resetPortColor() {
  currentFillColor = '#ffffff';
  currentFontColor = '#4bc68b';
  portState.style.fillColor = currentFillColor;
  portState.style.fontColor = currentFontColor;
  portState.shape?.apply(portState);
  portState.shape?.redraw();
  graph.cellRenderer.redrawLabel(portState, true);
}

function animateColors(fillColor, fontColor, duration, portState) {
  const startFillColor = portState.style.fillColor;
  const startFontColor = portState.style.fontColor;
  const fps = 100;
  const interval = 1000 / fps;
  const steps = Math.floor(duration * 1000 / interval);
  const deltaFillColor = calculateColorDelta(startFillColor, fillColor, steps);
  const deltaFontColor = calculateColorDelta(startFontColor, fontColor, steps);
  let step = 0;
  const intervalId = setInterval(function() {
    step++;
    const currentFillColor = calculateCurrentColor(startFillColor, deltaFillColor, step);
    const currentFontColor = calculateCurrentColor(startFontColor, deltaFontColor, step);
    portState.style.fillColor = currentFillColor;
    portState.style.fontColor = currentFontColor;
    portState.shape?.apply(portState);
    portState.shape?.redraw();
    graph.cellRenderer.redrawLabel(portState, true);
    if (step >= steps) {
      clearInterval(intervalId);
    }
  }, interval);
}

그냥 개 하드코딩이다.
나중에 누군가 보게 된다면 이마를 탁 치며 아이고 두야 를 외쳐도 할말 없을 것이다.

이번 버전에 들어서면서 다시 도전을 하게 되었는데, 버전을 거듭한 만큼 고민을 엄청 했다. 그래서 고안해낸 방법이 있는데 훨씬 깔끔해졌고, 범용성도 챙겼다.

이번에 구현한 방식은, 좌표 기반으로 해당 element가 위치하는 곳에 가상의 element를 만들어 그 element에 CSS 애니메이션을 적용시켰다.

CSS가 지원하는 방식이기에 JS애니메이션에 비해 세밀한 커스텀이 떨어지긴 하지만, 우리 프로젝트에 적용시키기에 아주 충분했다.

게다가, cell의 형태가 제각각이라 port 애니메이션 따로, warning icon 애니메이션, Tile 애니메이션 다 따로 만들었어야 했던 지난 방식과는 달리, 공통 함수로 만들어 쓸 수 있게 되었다.

fireAnimation(element, styleParameter, id) {
  // css 설치;
  const styleSheet = document.createElement("style");
  document.head.appendChild(styleSheet);
  styleSheet.sheet.insertRule(`
  @keyframes explode {
    0% { transform: scale(1); opacity: 1; }
    100% { transform: scale(${styleParameter.scale}); opacity: 0; }
  }`, 0);

  // animation div 설치;
  const rect = element.getBoundingClientRect();
  const fire = document.createElement("div");

  fire.id = id;
  fire.style.position = 'absolute';
  fire.style.width = `${styleParameter.width}px`;
  fire.style.height = `${styleParameter.height}px`;
  fire.style.borderRadius = `${styleParameter.radius}%`;
  fire.style.backgroundColor = styleParameter.color;
  fire.style.animation = `explode ${styleParameter.duration}s ease-out ${styleParameter.time}`;
  fire.style.left = `${rect.x - styleParameter.left}px`;
  fire.style.top = `${rect.y - styleParameter.top}px`;

  document.body.appendChild(fire);

  // animation div, css 제거;
  fire.addEventListener("animationend", () => {
    document.body.removeChild(fire);
    document.head.removeChild(styleSheet);
  });
}

만들고 나서 보니 이 방법 또한 걸리는 것이 있었다.
어떤 동작에 의해 화면이 움직이면 element를 따라다니지 않으므로 엉뚱한 곳에 떠있을 때가 있다.
때문에 강제로 캔슬 시켜야 한다.
강제로 캔슬 시키는 method를 만들어 아래의 케이스에 적용시켰다.

case 1) 선택된 타일 selection 풀릴 때(패널 캔슬)

case 2) 마우스 오른쪽 down => context menu open

case 3) 타일 grab 후 move 시 => 제자리에서 클릭할 때도 move로 인식하므로 moveStack(움직인 거리) > 2 이상일 때

case 4) 마우스 휠 up/down 동작으로 화면 이동 시

cancelFireAnimation(cell) {
  // port 애니메이션 종료 작업
  if (cell.tileName === 'BRANCH' || cell.tileName === 'ASK_A_QUESTION') {
    const ports = cell.children.filter(child => child.portType === 'metFlow');
    const editModePort = ports.find(port => port.editMode);
    const fire = document.getElementById(editModePort?.propertyId + 'fire');
                                         if (fire) {
      // console.log('is lighting!!!!!!!!!!');
      document.body.removeChild(fire);
    }
  }
  // warning icon 애니메이션 종료 작업
  const warningIcon = cell.children.find(child => child.style === 'warningIcon');
  if (warningIcon.visible) {
    const fire = document.getElementById(cell.propertyId + 'warning' + 'fire');
    if (fire) {
      // console.log('is lighting!!!!!!!!');
      document.body.removeChild(fire);
    }
  }
}
profile
Hound on the Code

0개의 댓글