<3/products/personal-site/remotion/scenes/

AvatarOrbit.tsx

import type { CSSProperties } from "react";
import { Img, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import type { AvatarOrbitProps } from "./AvatarOrbit.data";

export type { AvatarOrbitProps };

/**
 * A person (center) surrounded by their village — profile pics orbiting the
 * central one.
 *
 * Frame-driven, not CSS-driven: every position is a pure function of
 * `useCurrentFrame()`. One full revolution is mapped to the composition's full
 * duration, so the last frame lands one step before 360° and wraps cleanly to
 * frame 0 — a seamless loop by construction (verify in /videos/avatar-orbit by
 * comparing frame 0 with the final frame).
 *
 * Faces stay upright because each avatar is *positioned* on the circle rather
 * than the whole ring being rotated — no counter-rotation needed.
 */
export function AvatarOrbit({
  ownerSrc,
  villagerSrcs,
  radius,
  avatarSize,
  ownerSize,
  background,
}: AvatarOrbitProps) {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();

  // One revolution over the whole composition → seamless loop.
  const rotation = (frame / durationInFrames) * 2 * Math.PI;
  const count = villagerSrcs.length;

  // Each avatar is positioned from the frame's center (50%/50%) and offset onto
  // the circle. `maxWidth: none` defeats any global `img { max-width: 100% }`
  // reset (e.g. Tailwind preflight) that the host page applies but the render
  // bundle does not — without it the avatars collapse to zero width in-browser.
  const avatarStyle = (size: number): CSSProperties => ({
    position: "absolute",
    left: "50%",
    top: "50%",
    width: size,
    height: size,
    maxWidth: "none",
    display: "block",
    borderRadius: "50%",
    objectFit: "cover",
  });

  return (
    <div style={{ position: "absolute", inset: 0, background }}>
      {villagerSrcs.map((src, i) => {
        const angle = (2 * Math.PI * i) / count - Math.PI / 2 + rotation;
        const x = radius * Math.cos(angle);
        const y = radius * Math.sin(angle);
        return (
          <Img
            key={i}
            src={staticFile(src)}
            alt=""
            style={{
              ...avatarStyle(avatarSize),
              transform: `translate(-50%, -50%) translate(${x}px, ${y}px)`,
            }}
          />
        );
      })}

      <Img
        src={staticFile(ownerSrc)}
        alt=""
        style={{
          ...avatarStyle(ownerSize),
          transform: "translate(-50%, -50%)",
          zIndex: 2,
        }}
      />
    </div>
  );
}