npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@firere/motion

v1.2.1

Published

Motion for React

Downloads

7

Readme

@rbxts/react-motion

A port of @rbxts/motion to React.

Introduction

Simply animate UI from point A to B with Motion, a simple yet powerful animation library for roblox-ts & React, inspired by the popular framer-motion library for React.

The motion object

The core of Motion is the motion object. Think of it as a plain Roblox GUI element, supercharged with animation capabilities:

import motion from "@rbxts/react-motion";

Animating a Motion component is as straight-forward as setting the animate prop:

<motion.textbutton animate={{ TextSize: 24 }} />

When the animate prop changes, Motion will automatically generate an animation to the latest value. This animation will feel great by default, but it can be configured with the flexible transition prop.

Transitions

A transition defines how values animate from one state to another. If you've ever used tweens in Roblox before, then it's the same as using TweenInfo:

const [size, setSize] = useState(UDim2.fromOffset(200, 50));

return (
  <motion.textbutton
    animate={{ Size: size }}
    Event={{
      Activated: () => setSize(size => UDim2.fromOffset(size.X.Offset + 50, size.Y.Offset + 50)),
    }}
    Size={UDim2.fromOffset(200, 50)}
    Text="Increase the size of this button!"
    transition={{
      // each of these are also their respective defaults
      duration: 1,
      easingStyle: Enum.EasingStyle.Linear,
      easingDirection: "InOut", // strings can be casted to enums for convenience
      repeatCount: 0, // -1 for infinity
      reverse: false,
      delay: 0,
    }}  
  />
);

The initial prop

The initial prop defines the properties for a Motion component to be set at the beginning. It differs from just setting props normally, as you can reference variants (more on those later) and state without it changing:

const [size, setSize] = useState(UDim2.fromOffset(200, 50));

return (
  <motion.textbutton
    animate={{ Size: size }}
    initial={{ Size: size }}
    Event={{
      Activated: () => setSize(size => UDim2.fromOffset(size.X.Offset + 50, size.Y.Offset + 50)),
    }}
    Text="Increase the size of this button!"
  />
);

This will work exactly the same as the example above, however now you can reference the initial size directly without it rerendering each time.

To be honest, I'm not sure what the purpose of initial exactly is or why it was in the original @rbxts/motion, or if this is even how it's supposed to work - upstream doesn't have it properly implemented. I got ghosted when I tried to ask the original author, so this is here to stay I guess.

Variants

Variants are a bit like classes in CSS in that you can define props specific to each variant. They're applied based on the animate and initial props:

const [state, setState] = useState<"hover" | "default">("default");

return (
  <motion.textbutton
    animate={state}
    initial="default"
    Event={{
      MouseEnter: () => setState("hover"),
      MouseLeave: () => setState("default"),
    }}
    Size={UDim2.fromOffset(200, 50)}
    variants={{
      hover: {
        TextColor3: new Color3(0, 1, 0),
      },
      default: {
        TextColor3: new Color3(1, 1, 1),
      },
    }}
  />
);

Transitions can also be used in variants. If used in both variants and as a prop simultaneously, the transitions will be merged into one transition. In the case of a conflict between a variant transition and the transition prop, the variant transition will have priority over the transition prop and will overwrite it.

[!NOTE] This differs from the behaviour of @rbxts/motion, which instead prioritises the transition prop over variants' transition. If you're migrating from @rbxts/motion, make sure you account for this.

const [state, setState] = useState<"hover" | "default">("default")

const buttonVariants = {
  hover: {
    TextColor3: new Color3(0, 1, 0),
    transition={{ 
      duration: 20,
      easingStyle: Enum.EasingStyle.Elastic, // overwrites Quint
      easingDirection: Enum.EasingDirection.Out,
      reverses: true, // unique to the `hover` variant
    }}
  },
  default: {
    TextColor3: new Color3(1, 1, 1),
    transition={{ 
      duration: 1, // overwrites 20
      easingStyle: Enum.EasingStyle.Quad, // overwrites Quint
      easingDirection: Enum.EasingDirection.In, // unique to the `default` variant
      repeatCount: 2, // unique to the `default` variant
    }}
  },
};

return (
  <motion.textbutton
    animate={state}
    transition={{
      duration: 20,
      easingStyle: Enum.EasingStyle.Quint,
      delay: 0.5, // gets added to all variants
    }}
    Event={{
      MouseEnter: () => setState("hover"),
      MouseLeave: () => setState("default"),
    }}
    variants={buttonVariants}
  />
);

Copy & paste example using motion object (works with Hoarcekat)

import React, { useState } from "@rbxts/react";
import { createPortal, createRoot } from "@rbxts/react-roblox";
import motion from "@rbxts/react-motion";

const variants = {
  off: {
    BackgroundColor3: new Color3(1, 1, 1),
    Size: UDim2.fromOffset(300, 100),
    TextColor3: new Color3(0, 0, 0),
  },
  on: {
    BackgroundColor3: new Color3(0, 0, 0),
    Size: UDim2.fromOffset(300, 90),
    TextColor3: new Color3(1, 1, 1),
  }
};

const Button = () => {
  const [state, setState] = useState<"on" | "off">("off");

  return (
    <motion.textbutton
      animate={state}
      AnchorPoint={new Vector2(0.5, 0.5)}
      Event={{
        MouseEnter: () => setState("on"),
        MouseLeave: () => setState("off"),
      }}
      Position={UDim2.fromScale(0.5, 0.5)}
      Size={UDim2.fromOffset(300, 100)}
      Text="Hello"
      TextSize={20}
      transition={{
        duration: 0.3,
      }}
      variants={variants}
    >
      <motion.uicorner
        animate={{
          CornerRadius: new UDim(0, state === "on" ? 20 : 0),
        }}
        transition={{
          duration: 0.3,
        }}
      />
    </motion.textbutton>
  );
}

export = (target: Instance) => {
  const root = createRoot(new Instance("Folder"));
  root.render(createPortal(<Button />, target));

  return () => root.unmount();
}

useAnimation

That's not all Motion has to offer, though! If you'd like, you can use the useAnimation hook provided by Motion. This allows you to animate any component in your codebase without having to use the motion object or the animate prop, as it returns a setter for the variant:

const ref = useRef<Frame>();
const [variant, setVariant] = useAnimation(ref, {
  transition: {
    duration: 3,
    easingStyle: "Quad",
  },
  variants: {
    hover: {
      BackgroundColor3: new Color3(0, 1, 0),
      transition: {
        duration: 0.5,
      },
    },
    default: {
      BackgroundColor3: new Color3(1, 0, 0),
      transition: {
        duration: 3,
      },
    },
  },
});

useEffect(() => print("Variant changed:", variant), [variant]);

return (
  <frame
    BackgroundColor3={new Color3(1, 0, 0)}
    Event={{ 
      MouseEnter: () => setVariant("hover"),
      MouseLeave: () => setVariant("default"),
    }}
    ref={ref}
  />
);

Because useAnimation simply takes in a ref, you can animate any React component with Motion, even if the motion object doesn't have it! If you still want to use the same syntax as you would with the motion object but with a different element, Motion also exports withAnimation:

import { withAnimation } from "@rbxts/motion";

const Part = withAnimation("Part"); // this is a React component with Motion props
const part = <Part animate={} initial={} transition={} variants={} />

Copy & paste example using useAnimation (works with Hoarcekat)

import React, { useRef } from "@rbxts/react";
import { createPortal, createRoot } from "@rbxts/react-roblox";
import { useAnimation } from "@rbxts/react-motion";

const variants = {
  off: {
    BackgroundColor3: new Color3(1, 1, 1),
    Size: UDim2.fromOffset(300, 100),
    TextColor3: new Color3(0, 0, 0),
  },
  on: {
    BackgroundColor3: new Color3(0, 0, 0),
    Size: UDim2.fromOffset(300, 90),
    TextColor3: new Color3(1, 1, 1),
  }
};

const Button = () => {
  const button = useRef<TextButton>();
  const [variant, setVariant] = useAnimation(button, {
    variants,
    transition: {
      duration: 0.3
    }
  });

  const uiCorner = useRef<UICorner>();
  useAnimation(uiCorner, {
    animate: {
      CornerRadius: new UDim(0, variant === "on" ? 20 : 0),
    },
    transition: {
      duration: 0.3,
    },
  });

  return (
    <textbutton
      AnchorPoint={new Vector2(0.5, 0.5)}
      BackgroundColor3={new Color3(1, 1, 1)}
      Event={{
        MouseEnter: () => setVariant("on"),
        MouseLeave: () => setVariant("off"),
      }}
      Position={UDim2.fromScale(0.5, 0.5)}
      ref={button}
      Size={UDim2.fromOffset(300, 100)}
      Text="Hello"
      TextColor3={new Color3(0, 0, 0)}
      TextSize={20}
    >
      <uicorner ref={uiCorner} />
    </textbutton>
  );
}

export = (target: Instance) => {
  const root = createRoot(new Instance("Folder"));
  root.render(createPortal(<Button />, target));

  return () => root.unmount();
}