DEV Community

Cover image for Building Interactive Emoji Animations in React βš›οΈπŸŽ―
Matt Lewandowski
Matt Lewandowski

Posted on

Building Interactive Emoji Animations in React βš›οΈπŸŽ―

Ever wondered how to make your planning poker sessions more interactive? Let's dive into implementing a fun emoji-throwing animation system using React hooks and the Web Animation API. I'll show you how I built this feature for Kollabe, our free planning poker tool.

The Challenge

Planning poker sessions can get monotonous, especially in remote settings. While building Kollabe, we wanted to add interactive elements that would:

  • Keep team members engaged
  • Provide non-verbal feedback options
  • Make remote sessions feel more personal
  • Add a fun factor without compromising functionality

The Solution: Flying Emojis

planning poker emoji

We implemented an emoji throwing system that allows team members to react to estimates by throwing emojis across the screen. Here's how we built it:

The Complete Implementation

Here's the full, working implementation you can use in your own projects:

import { useCallback, useRef } from "react";

interface Point {
  x: number;
  y: number;
}

interface DOMRect {
  left: number;
  top: number;
  width: number;
  height: number;
}

/**
 * Generates a random number within a specified range.
 */
const getRandomNumber = (
  min = 0.1,
  max = 0.4,
  isPositive = Math.random() < 0.5,
): number => {
  const random = Math.random() * (max - min) + min;
  return isPositive ? random : -random;
};

/**
 * Calculates the center position of a DOM element.
 */
const getCenterPosition = (rect: DOMRect): Point => ({
  x: rect.left + rect.width / 2,
  y: rect.top + rect.height / 2,
});

/**
 * Calculates the target position with a random offset from the center.
 */
const getTargetPosition = (rect: DOMRect): Point => {
  const center = getCenterPosition(rect);
  return {
    x: center.x - getRandomNumber(0, 15, true),
    y: center.y - getRandomNumber(0, 25, true),
  };
};

/**
 * Calculates the midpoint between two points.
 */
const getMidpoint = (start: Point, target: Point): Point => ({
  x: (start.x + target.x) / 2,
  y: (start.y + target.y) / 2,
});

/**
 * Calculates the distance between two points.
 */
const getDistance = (start: Point, target: Point): number => {
  return Math.sqrt(
    Math.pow(target.x - start.x, 2) + Math.pow(target.y - start.y, 2),
  );
};

/**
 * Calculates the control point for the quadratic Bezier curve.
 */
const getControlPoint = (start: Point, target: Point): Point => {
  const midpoint = getMidpoint(start, target);
  const distance = getDistance(start, target);

  const angle = Math.atan2(target.y - start.y, target.x - start.x);
  const perpAngle = angle + Math.PI / 2;
  const controlPointDistance = distance * getRandomNumber(0.2, 0.4, true);

  return {
    x: midpoint.x + Math.cos(perpAngle) * controlPointDistance,
    y: midpoint.y + Math.sin(perpAngle) * controlPointDistance,
  };
};

/**
 * Creates and styles the emoji element.
 */
const createEmojiElement = (
  emoji: string,
  startPoint: Point,
): HTMLDivElement => {
  const emojiElement = document.createElement("div");
  Object.assign(emojiElement.style, {
    position: "fixed",
    fontSize: "24px",
    pointerEvents: "none",
    zIndex: "1",
    left: "0",
    top: "0",
    transform: `translate(${startPoint.x}px, ${startPoint.y}px) translate(-50%, -50%)`,
  });
  emojiElement.textContent = emoji;
  document.body.appendChild(emojiElement);
  return emojiElement;
};

/**
 * Calculates a point on a quadratic Bezier curve.
 */
const calculateBezierPoint = (
  start: Point,
  control: Point,
  target: Point,
  progress: number,
): Point => {
  const easeProgress = 1 - Math.pow(1 - progress, 2);
  return {
    x:
      Math.pow(1 - easeProgress, 2) * start.x +
      2 * (1 - easeProgress) * easeProgress * control.x +
      Math.pow(easeProgress, 2) * target.x,
    y:
      Math.pow(1 - easeProgress, 2) * start.y +
      2 * (1 - easeProgress) * easeProgress * control.y +
      Math.pow(easeProgress, 2) * target.y,
  };
};

/**
 * Custom hook for throwing emoji animations.
 */
export const useEmojiThrow = () => {
  const animationRef = useRef<number>();
  const emojiRef = useRef<HTMLDivElement | null>(null);

  const throwEmoji = useCallback(
    (emoji: string, sourceId: string, targetId: string) => {
      const sourceEl = document.getElementById(sourceId);
      const targetEl = document.getElementById(targetId);

      if (!sourceEl || !targetEl) return;

      const startRect = sourceEl.getBoundingClientRect();
      const targetRect = targetEl.getBoundingClientRect();

      const startPoint = getCenterPosition(startRect);
      const targetPoint = getTargetPosition(targetRect);
      const controlPoint = getControlPoint(startPoint, targetPoint);

      const emojiElement = createEmojiElement(emoji, startPoint);
      emojiRef.current = emojiElement;

      let startTime = performance.now();
      const duration = 1000; // Animation duration in milliseconds

      const animate = (currentTime: number) => {
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / duration, 1);

        const position = calculateBezierPoint(
          startPoint,
          controlPoint,
          targetPoint,
          progress,
        );

        emojiElement.style.transform = 
          `translate(${position.x}px, ${position.y}px) translate(-50%, -50%)`;

        if (progress < 1) {
          animationRef.current = requestAnimationFrame(animate);
        } else {
          emojiElement.style.transform = 
            `translate(${targetPoint.x}px, ${targetPoint.y}px) translate(-50%, -50%)`;
          setTimeout(() => {
            emojiElement.remove();
          }, duration);
        }
      };

      requestAnimationFrame(() => {
        startTime = performance.now();
        animate(startTime);
      });
    },
    [],
  );

  const cleanup = useCallback(() => {
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current);
    }
    if (emojiRef.current) {
      emojiRef.current.remove();
    }
  }, []);

  return { throwEmoji, cleanup };
};
Enter fullscreen mode Exit fullscreen mode

How to Use It

Here's a complete example of how to implement the emoji throwing in your React component:

import React from 'react';
import { useEmojiThrow } from './useEmojiThrow';

const PlanningPokerRoom: React.FC = () => {
  const { throwEmoji } = useEmojiThrow();

  const handleEmojiClick = (emoji: string) => {
    // Assuming you have elements with these IDs in your DOM
    throwEmoji(emoji, "source-player-avatar", "target-player-avatar");
  };

  return (
    <div className="poker-room">
      <div id="source-player-avatar" className="avatar">
        Player 1
      </div>

      <div className="emoji-controls">
        <button onClick={() => handleEmojiClick("πŸŽ‰")}>Throw Confetti</button>
        <button onClick={() => handleEmojiClick("πŸ‘")}>Throw Thumbs Up</button>
        <button onClick={() => handleEmojiClick("❀️")}>Throw Heart</button>
      </div>

      <div id="target-player-avatar" className="avatar">
        Player 2
      </div>
    </div>
  );
};

export default PlanningPokerRoom;
Enter fullscreen mode Exit fullscreen mode

Key Features

  1. Natural Motion: Uses quadratic Bezier curves for smooth, arc-like motion
  2. Randomization: Adds slight variations to target positions and trajectories
  3. Performance: Uses requestAnimationFrame for smooth animations
  4. Cleanup: Includes proper cleanup of animations and DOM elements
  5. TypeScript Support: Fully typed for better developer experience

Technical Details

The animation works by:

  1. Calculating start and end positions from DOM elements
  2. Creating a temporary emoji element
  3. Animating it along a Bezier curve
  4. Cleaning up after the animation completes

The useEmojiThrow hook handles all the complexity, providing a simple interface for throwing emojis between any two elements on your page.

See It in Action

Want to see this code running in a production environment? Check out our demo room where you can try throwing emojis at bot players, or start your own planning poker session at Kollabe.

Performance Considerations

  • Animations use CSS transforms for better performance
  • Elements are properly cleaned up to prevent memory leaks
  • Calculations are optimized and cached where possible
  • Animation frames are canceled on cleanup

Potential Customizations

You can easily modify the code to:

  • Adjust animation duration
  • Change emoji size
  • Modify trajectory randomization
  • Add rotation or scaling effects
  • Implement different easing functions

That's Everything

Adding interactive elements like emoji throwing can transform standard planning poker sessions into engaging team experiences. Feel free to use this code in your own projects, and if you're looking for a ready-to-use solution, try Kollabe for your team's planning sessions.

Top comments (0)