uriel avalos

SpriteMotion Comics

Note: I thought generative AI would make this unnecessary. (And maybe it will...in a few months. 🤣) But for now it's super fun.

IMO the evolution of comics is not motion comics. Have you ever tried making one?

You need specialized training in animation and tools---meaning it's not likely something you can do these on a daily basis. But daily comics, namely the daily comics strip, are about.

The title of biggest contender for new comics goes to Runway Gen 2.

But what if we had a simple, low tech, manual way to bring sprites to life?

Hello, SpriteMotion Comics

The idea is pretty simple. Run DOM elements on a physics engine. Give DOM elements position and velocity props.

<World>
  <MotionImg src="running-man.png" x={0} y={0} vx={3} vy={0} />
  <MotionDiv x={0} y={0} width={25} height={5} />
</World>

Boom!

And since this is JSX, we can do cool stuff like this:

const CryingPortrait = () => {
  return (
    <World>
      <img src="portrait.png" />
      {Array.from({ length: 1000 }).map((_, key) => (
        <MotionImg
          key={key}
          src="tear.png"
          vx={Math.random()}
          vy={Math.random()}
        />
      ))}
    </World>
  );
};

The World component implementation is less than 100 lines. And it's nothing too suprising. Ditto MotionImg. You can see the code here.

It uses matter-js for the physics engine.

A live example

This is the library in action:

An explanation is in order.

  • The main thing is that we do the React technique of resetting state by changing the key. In this case, we change the key every 700ms, effectively, re-creating the tears.
  • (Note: you may notice a flicker because the images are redownloaded each state reset!. This is obviously bad. In prod code, you'll want to use more targetted state resetting.)
  • The "tears" are just a MotionDiv with random velocity, all originating from the location of the eyes.
const random = (scale: number) =>
  Math.random() * scale * (Math.random() > 0.5 ? -1 : 1);

const rightEye = { startX: 125, startY: 125 };
const leftEye = { startX: 55, startY: 125 };

export const TearsInTheRainInner = () => {
  return (
    <World>
      <img src="./tears-in-the-rain.png" />
      {Array.from({ length: 100 }).map((_, index) => {
        const eye = index % 2 ? rightEye : leftEye;
        return (
          <MotionDiv
            key={Math.random()}
            height={5}
            width={5}
            {...eye}
            vx={random(3)}
            vy={random(3)}
            style={{
              borderRadius: "5px",
              borderColor: "blue",
              background: "blue",
            }}
          />
        );
      })}
    </World>
  );
};

export const TearsInTheRainFixture = () => {
  const [key, setKey] = useState(Math.random());

  useEffect(() => {
    const tears = () => setKey(Math.random());
    const unsubscribe = setInterval(tears, 800);
    return () => {
      clearInterval(unsubscribe);
    };
  });

  return <TearsInTheRainInner key={key} />;
};