React 动效 Framer motion,给你的页面添加一点动感

在这里插入图片描述
Framer motion的核心API是motion的组件。每个HTML和SVG标签都有对应的motion组件。

他们渲染的结果与对应的原生组件完全一致,并在其之上增加了一些动画和手势相关的props。
比如:

<motion.div />
<motion.span />
<motion.h1 />
<motion.svg />

使用与平常的插件相差无几。

npm install framer-motion

当然,这里还是有一点不一样的,如果直接在页面中使用是会报错的。因此,需要暴露出 webpack 配置。执行 npm run eject
在 config / webpack.config.js 中,添加这行代码

{
   test: /\.mjs$/,
   include: /node_modules/,
   type: 'javascript/auto',
},

重新启动项目即可。

官网 API 文档地址👉:https://www.framer.com/docs/

mount 动画

实现一个组件 mount 时的下降显现效果,需要使用 motion 组件的 initial 和 animate 属性。

  • initial 定义组件的初始状态

  • animate定义组件mount时的动画效果。如果其值与initial不同,则会产生过渡的动画效果

import React from 'react'
import { motion } from 'framer-motion'

function App() {
  return (
    <motion.div
      style={{
        width: '100px',
        height: '100px',
        backgroundColor: '#5b1b70'
      }}
      initial={{ opacity: 0, y: -50 }}
      animate={{ opacity: 1, y: 0 }}
    >
      Hello World!
    </motion.div>

  )
}

export default App

Framer Motion 检测到 initialanimate 中有 value 不同的 key,对这些 key 执行过渡效果。

例子中 y 轴方向距离会从 -50 变为 0,透明度从 0 变为 1。

unmount 动画

实现 unmount 动画效果,需要将组件包裹在 <AnimatePresence/> 内。

需要指定 exit 属性,当组件 unmount 时,会执行从 animateexit 的动画效果。

import React from 'react'
import { motion, AnimatePresence } from 'framer-motion'

function App() {

  return (
    <AnimatePresence>
      <motion.div
        style={{
          width: '100px',
          height: '100px',
          backgroundColor: '#5b1b70'
        }}
        exit={{ opacity: 0, y: -50 }}
        initial={{ opacity: 0, y: -50 }}
        animate={{ opacity: 1, y: 0 }}
      >
        Hello World!
      </motion.div>
    </AnimatePresence>
  )
}

export default App

可以实现如下的动画效果

在这里插入图片描述

import "./index.css";
import * as React from "react";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { initialTabs as tabs } from "./ingredients";

export default function App() {
  const [selectedTab, setSelectedTab] = useState(tabs[0]);

  return (
    <div className="window">
      <nav>
        <ul>
          {tabs.map((item) => (
            <li
              key={item.label}
              className={item === selectedTab ? "selected" : ""}
              onClick={() => setSelectedTab(item)}
            >
              {`${item.icon} ${item.label}`}
              {item === selectedTab ? (
                <motion.div className="underline" layoutId="underline" />
              ) : null}
            </li>
          ))}
        </ul>
      </nav>
      <main>
        <AnimatePresence exitBeforeEnter>
          <motion.div
            key={selectedTab ? selectedTab.label : "empty"}
            animate={{ opacity: 1, y: 0 }}
            initial={{ opacity: 0, y: 20 }}
            exit={{ opacity: 0, y: -20 }}
            transition={{ duration: 0.15 }}
          >
            {selectedTab ? selectedTab.icon : "😋"}
          </motion.div>
        </AnimatePresence>
      </main>
    </div>
  );
}

index.css

body {
  --accent: #8855ff;
  width: 100vw;
  height: 100vh;
  background: var(--accent);
  overflow: hidden;
  padding: 0;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.window {
  width: 480px;
  height: 360px;
  border-radius: 10px;
  background: white;
  overflow: hidden;
  box-shadow: 0 1px 1px hsl(0deg 0% 0% / 0.075),
    0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075),
    0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075);
  display: flex;
  flex-direction: column;
}

nav {
  background: #fdfdfd;
  padding: 5px 5px 0;
  border-radius: 10px;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: 1px solid #eeeeee;
  height: 44px;
}

.tabs {
  flex-grow: 1;
  display: flex;
  justify-content: flex-start;
  align-items: flex-end;
  flex-wrap: nowrap;
  width: 100%;
}

main {
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 128px;
  flex-grow: 1;
  user-select: none;
}

ul,
li {
  list-style: none;
  padding: 0;
  margin: 0;
  font-family: "Poppins", sans-serif;
  font-weight: 500;
  font-size: 14px;
}

ul {
  display: flex;
  width: 100%;
}

li {
  border-radius: 5px;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  width: 100%;
  padding: 10px 15px;
  position: relative;
  background: white;
  cursor: pointer;
  height: 24px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex: 1;
  min-width: 0;
  position: relative;
  user-select: none;
}

.underline {
  position: absolute;
  bottom: -1px;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--accent);
}

li.selected {
  background: #eee;
}

li button {
  width: 20px;
  height: 20px;
  border: 0;
  background: #fff;
  border-radius: 3px;
  display: flex;
  justify-content: center;
  align-items: center;
  stroke: #000;
  margin-left: 10px;
  cursor: pointer;
  flex-shrink: 0;
}

.background {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 300px;
  background: #fff;
}

.add-item {
  width: 30px;
  height: 30px;
  background: #eee;
  border-radius: 50%;
  border: 0;
  cursor: pointer;
  align-self: center;
}

.add-item:disabled {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}

ingredients.js

export const allIngredients = [
    { icon: "🍅", label: "Tomato" },
    { icon: "🥬", label: "Lettuce" },
    { icon: "🧀", label: "Cheese" },
    { icon: "🥕", label: "Carrot" },
    { icon: "🍌", label: "Banana" },
    { icon: "🫐", label: "Blueberries" },
    { icon: "🥂", label: "Champers?" }
];

const [tomato, lettuce, cheese] = allIngredients;
export const initialTabs = [tomato, lettuce, cheese];

export function getNextIngredient(
    ingredients
) {
    const existing = new Set(ingredients);
    return allIngredients.find((ingredient) => !existing.has(ingredient));
}

参考文章:
https://juejin.cn/post/6934657845094776845
https://juejin.cn/post/6945015156933918734
https://juejin.cn/post/6907054067420233742

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值