1: Toogle菜单

关键知识点:

delayChildren 、 variants 、延时动画

视频演示:

1.mp4 (247.27KB)

源文件:

01 Accordion-Menu copy.framerx.zip

代码演示

  1. import { Data, Override } from "framer"
  2. // View related tutorial: https://www.framer.com/learn/lesson/build-an-animated-accordion/
  3. const appState = Data({
  4. open: false,
  5. })
  6. export function ListContainer(props): Override {
  7. const containerVariants = {
  8. closed: { height: 71 },
  9. open: {
  10. height: 332,
  11. transition: { staggerChildren: 0.14, delayChildren: 0.1 },
  12. },
  13. }
  14. return {
  15. initial: "closed",
  16. animate: appState.open ? "open" : "closed",
  17. variants: containerVariants,
  18. onTap() {
  19. appState.open = !appState.open
  20. },
  21. }
  22. }
  23. export function ListItem(props): Override {
  24. const itemVariants = {
  25. closed: { opacity: 0, y: 40 },
  26. open: { opacity: 1, y: 0 },
  27. }
  28. return {
  29. variants: itemVariants,
  30. transition: { ease: "easeInOut", duration: 0.2 },
  31. }
  32. }
  33. export function Cheveron(): Override {
  34. const cheveronVariants = {
  35. closed: { rotate: 0 },
  36. open: { rotate: 180 },
  37. }
  38. return {
  39. initial: "closed",
  40. animate: appState.open ? "open" : "closed",
  41. variants: cheveronVariants,
  42. transition: { ease: "easeInOut", duration: 0.2 },
  43. }
  44. }

2: 菜单切换

关键知识点:

data 、useAnimation 、 controls

视频演示:

2.mp4 (271.28KB)

源文件:

02 Align-Animation.framerx.zip

代码演示

  1. import { Data, Override, useAnimation } from "framer"
  2. const appState = Data({
  3. state: "",
  4. })
  5. export function Left(): Override {
  6. return {
  7. onTap() {
  8. appState.state = "left"
  9. },
  10. }
  11. }
  12. export function Center(): Override {
  13. return {
  14. onTap() {
  15. appState.state = "center"
  16. },
  17. }
  18. }
  19. export function Right(): Override {
  20. return {
  21. onTap() {
  22. appState.state = "right"
  23. },
  24. }
  25. }
  26. export function firstLine(): Override {
  27. const controls = useAnimation()
  28. if (appState.state === "left") {
  29. controls.start({ x: 0 })
  30. } else if (appState.state === "center") {
  31. controls.start({ x: 78 })
  32. } else if (appState.state === "right") {
  33. controls.start({ x: 156 })
  34. }
  35. return {
  36. animate: controls,
  37. transition: { type: "spring", mass: 1.1, velocity: 350 },
  38. }
  39. }
  40. export function secondLine(): Override {
  41. const controls = useAnimation()
  42. if (appState.state === "left") {
  43. controls.start({ x: 0 })
  44. } else if (appState.state === "center") {
  45. controls.start({ x: 81 })
  46. } else if (appState.state === "right") {
  47. controls.start({ x: 162 })
  48. }
  49. return {
  50. animate: controls,
  51. transition: { type: "spring", mass: 0.8, velocity: 200 },
  52. }
  53. }
  54. export function thirdLine(): Override {
  55. const controls = useAnimation()
  56. if (appState.state === "left") {
  57. controls.start({ x: 0 })
  58. } else if (appState.state === "center") {
  59. controls.start({ x: 78 })
  60. } else if (appState.state === "right") {
  61. controls.start({ x: 156 })
  62. }
  63. return {
  64. animate: controls,
  65. transition: { type: "spring", mass: 1, velocity: 120 },
  66. }
  67. }

3: Drag-Handle

关键知识点:

useTransform, motionValue, drag

图片演示:

截屏2019-12-26下午4.16.14.png

源文件:

03 Drag-Handle.framerx.zip

代码演示

import { Data, Override, useTransform, motionValue } from "framer"

// View the full tutorial: https://www.framer.com/learn/lesson/build-a-drag-handle/

const handleX = motionValue(0)

export function Handle(): Override {
    return {
        x: handleX,
        drag: "x",
        dragConstraints: {
            left: -113,
            right: 113,
        },
        dragElastic: 0.1,
        dragMomentum: false,
    }
}

export function LeftPanel(props): Override {
    const { width } = props
    const panelWidth = useTransform(handleX, value => {
        return width + value
    })

    return {
        width: panelWidth,
    }
}

export function RightPanel(props): Override {
    const { width } = props
    const panelWidth = useTransform(handleX, value => {
        return width - value
    })

    return {
        width: panelWidth,
    }
}

6:heart点赞动效

关键知识点:

useAnimation

视频演示:

06 heart.mp4 (1.01MB)

源文件:

06 heart.framerx.zip

代码演示

import { Data, Override, useAnimation } from "framer"

// Store the animating state and heart ids
const appState = Data({
    animating: false,
    heartsId: [],
})

// Create a function to get a random integer
function getRandomInt(min, max) {
    min = Math.ceil(min)
    max = Math.floor(max)
    return Math.floor(Math.random() * (max - min)) + min
}

// Scale the button when tapped and set the animating state to true
export function HeartButton(): Override {
    return {
        whileTap: { scale: 1.2 },
        onTap() {
            appState.animating = true
        },
    }
}

export function Heart(props): Override {
    // Get the index of the heart
    const { id } = props
    appState.heartsId.length == 5 ? null : appState.heartsId.push(id)

    let index = appState.heartsId.indexOf(id)

    // Randomly generate rotate and x travel distance values for each heart
    let rotate = getRandomInt(-10, 10)
    let travelX = (getRandomInt(0, 2) * 2 - 1) * 14

    // Create an animation control for the heart
    const controls = useAnimation()

    if (appState.animating === true) {
        // Scale the heart up quickly when the animation starts
        controls.start({
            scale: 1,
            transition: {
                ease: "easeInOut",
                duration: 0.2,
                delay: 0.4 * index,
            },
        })
        // Float the heart to the top
        controls.start({
            y: -660,
            opacity: 0,
            transition: {
                ease: "easeOut",
                duration: 5.6,
                delay: 0.35 * index,
            },
        })
        // Rotate and make the heart float left and right as it goes up
        controls.start({
            rotate: [rotate, -rotate, rotate, -rotate, rotate],
            x: [travelX, -travelX, travelX, -travelX, travelX],
            transition: {
                ease: "easeOut",
                duration: 3,
                delay: 0.4 * index,
            },
        })
        // Wait for the duration of the animation then turn the animating state to false
        if (appState.heartsId.indexOf(id) === 0) {
            setTimeout(() => {
                appState.animating = false
            }, 400 * 4 + 2200)
        }
    }
    // Revert the heart back to its original place on completion
    else {
        controls.start({
            x: 0,
            y: 0,
            rotate: 0,
            scale: 0,
            opacity: 1,
            transition: { duration: 0 },
        })
    }

    return {
        initial: { x: 0, y: 0, rotate: 0, scale: 0, opacity: 1 },
        animate: controls,
    }
}

7: 点赞计数

关键知识点

useAnimation , keyfrme

视频演示

07.mp4 (553.37KB)

源文件

07 Like-Counter.framerx.zip

代码展示

import { Data, Override, useAnimation } from "framer"

const appState = Data({
    counter: 0,
    tapped: false,
})

export function Container(): Override {
    return {
        onTap() {
            // Start the animation
            appState.tapped = true
            // Update the counter
            appState.counter++
        },
    }
}

export function Heart(): Override {
    const heartAnimation = useAnimation()

    // Sequence for heart animation
    async function heartSequence() {
        await heartAnimation.start(
            { scale: 1.3 },
            { ease: "easeInOut", duration: 0.2 }
        )
        await heartAnimation.start(
            { scale: 1 },
            { ease: "easeInOut", duration: 0.2 }
        )
        await (() => {
            // Update the tapped value to false when the animation has finished
            return new Promise(() => {
                appState.tapped = false
            })
        })
    }

    // Only play the sequence if the container has been tapped
    appState.tapped ? heartSequence() : null

    return {
        initial: { scale: 1 },
        animate: heartAnimation,
    }
}

export function Counter(): Override {
    return {
        text: appState.counter,
    }
}

8: Long-Press-Menu

关键知识点

视频演示

08.mp4 (1.67MB)

源文件

08 Long-Press-Menu.framerx.zip

代码展示

import { Data, Override, useAnimation } from "framer"

const appState = Data({
    holdTap: false,
    pointX: 0,
    pointY: 0,
    menuId: [],
})

export function Container(): Override {
    return {
        onTapStart(event, info) {
            // Convert the x, y point information so that it is relative to the top left of the container
            let convertX = info.point.x - 32 - 20
            let convertY = info.point.y - 534 - 20

            // If tapped within the tappable area, set the tapping state to true
            if (convertX > 50 && convertX < 282 && convertY > 50) {
                appState.pointX = convertX
                appState.pointY = convertY
                appState.holdTap = true
            }
        },
        onTap() {
            // When the tap ends, set the tapping state to false
            appState.holdTap = false
        },
    }
}

export function Menu(props): Override {
    // Get the individual ids of each element and store it in an array
    const { id } = props
    if (appState.menuId.length < 3) {
        appState.menuId.push(id)
    }

    // Add animation controls to the element
    const controls = useAnimation()

    async function startAnimation() {
        // Set the position of the element to the x, y point of the tap
        await controls.start({
            x: appState.pointX,
            y: appState.pointY,
            transition: { duration: 0 },
        })
        // Animate the first menu item
        if (appState.menuId.indexOf(id) === 0) {
            await controls.start({
                x: appState.pointX - 40,
                y: appState.pointY - 30,
                scale: 1,
                opacity: 1,
                transition: { duration: 0.2 },
            })
        }
        // Animate the second menu item
        else if (appState.menuId.indexOf(id) === 1) {
            await controls.start({
                x: appState.pointX,
                y: appState.pointY - 50,
                scale: 1,
                opacity: 1,
                transition: { duration: 0.2 },
            })
        }
        // Animate the third menu item
        else {
            await controls.start({
                x: appState.pointX + 40,
                y: appState.pointY - 30,
                scale: 1,
                opacity: 1,
                transition: { duration: 0.2 },
            })
        }
    }

    // Animate back into the initial position
    function endAnimation() {
        controls.start({
            x: appState.pointX,
            y: appState.pointY,
            scale: 0,
        })
        controls.start({
            opacity: 0,
            transition: { duration: 0.14 },
        })
    }

    // Begin and end the animation based on the tapping state
    if (appState.holdTap) {
        startAnimation()
    } else {
        endAnimation()
    }

    return {
        animate: controls,
        // Scale the menu items when hovered upon
        onHoverStart() {
            controls.start({
                scale: 1.1,
            })
        },
        onHoverEnd() {
            controls.start({
                scale: 1,
            })
        },
    }
}

9: Perspective-3D

关键知识点

视频演示

09.mp4 (385.89KB)

源文件

09 Perspective-3D.framerx.zip

代码展示

import { Data, Override } from "framer"

const appState = Data({
    tapped: false,
})

export function Container(): Override {
    return {
        style: { perspective: 2000 },
    }
}

export function CardA(): Override {
    return {
        // Create a gap between the cards by moving up the frame by 60%
        style: { transformOrigin: "center 60%" },
        initial: { rotateX: 0 },
        animate: appState.tapped
            ? {
                  rotateX: -40,
                  boxShadow: "0px -10px 30px 0px rgba(0, 0, 0, 0.05)",
              }
            : { rotateX: 0, boxShadow: "0px 0px 0px 0px rgba(0, 0, 0, 0)" },
        onTap() {
            appState.tapped = !appState.tapped
        },
    }
}

export function CardB(): Override {
    return {
        style: { transformOrigin: "center 0%" },
        initial: { rotateX: 0 },
        animate: appState.tapped
            ? {
                  rotateX: -40,
                  boxShadow: "0px -10px 30px 0px rgba(0, 0, 0, 0.05)",
              }
            : { rotateX: 0, boxShadow: "0px 0px 0px 0px rgba(0, 0, 0, 0)" },
    }
}

export function CardC(): Override {
    return {
        // Create a gap between the cards by moving down the frame by 60%
        style: { transformOrigin: "center -60%" },
        initial: { rotateX: 0 },
        animate: appState.tapped
            ? {
                  rotateX: -40,
                  boxShadow: "0px -10px 30px 0px rgba(0, 0, 0, 0.05)",
              }
            : {
                  rotateX: 0,
                  boxShadow: "0px 0px 0px 0px rgba(0, 0, 0, 0)",
              },
    }
}

10: Perspective-Tilt

关键知识点

视频演示

10.mp4 (701.32KB)

源文件

10 Perspective-Tilt.framerx.zip

11: Progress-Bar

关键知识点

视频演示

11.mp4 (234.69KB)

源文件

11 Progress-Bar.framerx.zip

代码展示

import { Data, Override } from "framer"

const appState = Data({
    upload: "false",
    uploadCount: 0,
})

export function Bar(): Override {
    return {
        initial: { width: "0%" },
        // Animate the progress bar
        animate:
            appState.upload === "uploading" || appState.upload === "complete"
                ? { width: "100%" }
                : { width: "0%" },
        transition: { ease: "linear", duration: 1 },
        // Change the state to complete when the animation is finished
        onAnimationComplete: () => {
            appState.upload = "complete"
        },
    }
}

export function Button(): Override {
    return {
        onTap() {
            // Only trigger the animation once
            if (appState.uploadCount === 0) {
                appState.upload = "uploading"
                appState.uploadCount = 1
            }
        },
    }
}

export function Upload(): Override {
    return {
        initial: { opacity: 1 },
        animate: {
            opacity: appState.upload === "false" ? 1 : 0,
        },
        transition: { ease: "easeInOut", duration: 0.1 },
    }
}

export function UploadDisabled(): Override {
    return {
        initial: { opacity: 0 },
        animate: {
            opacity: appState.upload === "uploading" ? 1 : 0,
        },
        transition: { ease: "easeInOut", duration: 0.1 },
    }
}

export function Check(): Override {
    return {
        initial: { opacity: 0 },
        animate: {
            opacity: appState.upload === "complete" ? 1 : 0,
        },
        transition: { ease: "easeInOut", duration: 0.1 },
    }
}

12 : Pull-to-Refresh

关键知识点

视频演示

12.mp4 (1.25MB)

源文件

12 Pull-to-Refresh.framerx.zip

代码展示

import {
    Data,
    Override,
    motionValue,
    transform,
    useTransform,
    useAnimation,
} from "framer"

const appState = Data({
    loading: "false",
})

// Set up to track the offset of the content as an animatable value
const scrollY = motionValue(0)

export function Scroll(): Override {
    const controls = useAnimation()

    return {
        contentOffsetY: scrollY,

        // Only enable drag when the spinner is not loading
        dragEnabled: appState.loading === "false",

        // When the layer has been dragged past a certain point, begin the loading animation
        onPanEnd: () => {
            if (appState.loading === "false" && scrollY.get() > 116) {
                // Keep the layer pinned at 116px from the top while loading
                appState.loading = "true"
                controls.start({ y: 116 })

                // When the animation is complete, animate the layer back to the top and set the loading state to false
                setTimeout(() => {
                    appState.loading = "false"
                    controls.start({ y: 0 })
                }, 1500)
            } else {
                controls.start({ y: 0 })
            }
        },
        scrollAnimate: controls,
    }
}

export function Spinner(): Override {
    // Change the opacity of the spinner based on how much the layer has been dragged
    const spinnerOpacity = useTransform(scrollY, [76, 116], [0, 1])

    // Change the y position of the spinner based on how much the layer has been dragged
    const spinnerY = useTransform(scrollY, [76, 116], [-20, 0])

    return {
        opacity: spinnerOpacity,
        y: spinnerY,
    }
}

13 :Shuffle 随机加载

关键知识点

视频演示

13.mp4 (558.83KB)

源文件

13 Shuffle.framerx.zip

14: 手写效果

关键知识点

视频演示

14.mp4 (261.91KB)

源文件

14 Signature-Pad.framerx.zip

15 : Slider

关键知识点

视频演示

15.mp4 (1.06MB)

源文件

15 Slider.framerx.zip

代码展示

import * as React from "react"
import { Override, Data } from "framer"

const appState = Data({
    number: "0",
})

export function Slider(): Override {
    return {
        onValueChange: number => {
            appState.number = Math.floor(number).toString()
        },
    }
}

export function Number(): Override {
    return {
        text: appState.number,
    }
}

16: Long-Press-Menu

关键知识点

视频演示

16.mp4 (548.71KB)

源文件

16 Sound-Effects.framerx.zip

代码展示

import * as React from "react"
import { Frame, ControlType, addPropertyControls } from "framer"
import { url } from "framer/resource"
import { facebook, google } from "./kits"

function isPathAbsolute(path: string): boolean {
    return /^(?:\/|[a-z]+:\/\/)/.test(path)
}

export const sounds = Object.assign(facebook, { "———————": "" }, google)

export function playSound(soundName: string | HTMLAudioElement): void {
    if (typeof soundName === "string") {
        let sound: HTMLAudioElement

        if (isPathAbsolute(soundName)) {
            sound = new Audio(soundName)
        } else {
            sound = new Audio(
                url(
                    soundName.replace(
                        "./",
                        "./node_modules/@framer/ismael.sounds/"
                    )
                )
            )
        }

        // @ts-ignore
        sound.cloneNode(false).play()
    } else {
        // @ts-ignore
        soundName.cloneNode(false).play()
    }
}

function playSoundLocal(soundName: string | HTMLAudioElement): void {
    if (typeof soundName === "string") {
        let sound: HTMLAudioElement = new Audio(url(soundName))

        // @ts-ignore
        sound.cloneNode(false).play()
    }
}

export function Sounds(props: any): any {
    const {
        eventName,
        soundName,
        eventName2,
        soundName2,
        soundName3,
        eventName3,
        soundName4,
        eventName4,
        soundName5,
        eventName5,
        triggers,
        ...rest
    } = props

    let event, event2, event3, event4, event5

    event = { [eventName]: () => playSoundLocal(sounds[soundName]) }

    if (triggers >= 2) {
        event2 = { [eventName2]: () => playSoundLocal(sounds[soundName2]) }
    }

    if (triggers >= 3) {
        event3 = { [eventName3]: () => playSoundLocal(sounds[soundName3]) }
    }

    if (triggers >= 4) {
        event4 = { [eventName4]: () => playSoundLocal(sounds[soundName4]) }
    }

    if (triggers >= 5) {
        event5 = { [eventName5]: () => playSoundLocal(sounds[soundName5]) }
    }

    if (!props.children.length) {
        return <Frame size={"100%"}>Connect to a frame</Frame>
    }

    return (
        <Frame
            size={"100%"}
            background="transparent"
            {...event}
            {...event2}
            {...event3}
            {...event4}
            {...event5}
            {...rest}
        >
            {props.children}
        </Frame>
    )
}

const events = [
    "onTap",
    "whileTap",
    "whileHover",
    "onAnimationStart",
    "onAnimationEnd",
    "onDrag",
    "onDragStart",
    "onDragEnd",
    "onFocus",
]

addPropertyControls(Sounds, {
    triggers: {
        type: ControlType.Number,
        defaultValue: 1,
        step: 1,
        min: 1,
        max: 5,
        displayStepper: true,
        title: "Triggers",
    },
    eventName: {
        type: ControlType.Enum,
        defaultValue: events[0],
        options: events,
        optionTitles: events,
        title: "Event",
    },
    soundName: {
        type: ControlType.Enum,
        defaultValue: "",
        options: Object.keys(sounds),
        title: "Sound",
    },
    eventName2: {
        type: ControlType.Enum,
        defaultValue: "",
        options: events,
        optionTitles: events,
        title: "Event 2",
        hidden(props) {
            return props.triggers < 2
        },
    },
    soundName2: {
        type: ControlType.Enum,
        defaultValue: "",
        options: Object.keys(sounds),
        title: "Sound 2",
        hidden(props) {
            return props.triggers < 2
        },
    },
    eventName3: {
        type: ControlType.Enum,
        defaultValue: "",
        options: events,
        optionTitles: events,
        title: "Event 3",
        hidden(props) {
            return props.triggers < 3
        },
    },
    soundName3: {
        type: ControlType.Enum,
        defaultValue: "",
        options: Object.keys(sounds),
        title: "Sound 3",
        hidden(props) {
            return props.triggers < 3
        },
    },
    eventName4: {
        type: ControlType.Enum,
        defaultValue: "",
        options: events,
        optionTitles: events,
        title: "Event 4",
        hidden(props) {
            return props.triggers < 4
        },
    },
    soundName4: {
        type: ControlType.Enum,
        defaultValue: "",
        options: Object.keys(sounds),
        title: "Sound 4",
        hidden(props) {
            return props.triggers < 4
        },
    },
    eventName5: {
        type: ControlType.Enum,
        defaultValue: "",
        options: events,
        optionTitles: events,
        title: "Event 5",
        hidden(props) {
            return props.triggers < 5
        },
    },
    soundName5: {
        type: ControlType.Enum,
        defaultValue: "",
        options: Object.keys(sounds),
        title: "Sound 5",
        hidden(props) {
            return props.triggers < 5
        },
    },
})

17 : Toast-Prompt

关键知识点

视频演示

17.mp4 (418.3KB)

源文件

17 Toast-Prompt.framerx.zip

代码展示

import { Data, Override } from "framer"

const appState = Data({
    clicked: false,
})

export function Button(): Override {
    return {
        onTap() {
            // Ensure the animation only runs once
            if (appState.clicked === false) {
                // Toggle the clicked state to true
                appState.clicked = true
                // Toggle back to false after the animation has ended
                setTimeout(() => {
                    appState.clicked = false
                }, 400 * 2 + 2000)
            }
        },
    }
}

export function Toast(): Override {
    return {
        initial: { opacity: 0, y: 100 },
        animate:
            appState.clicked === true
                ? { opacity: 1, y: 0 }
                : { opacity: 0, y: 100 },
        transition: {
            ease: "easeInOut",
            duration: 0.4,
            flip: 1,
            repeatDelay: 2,
        },
    }
}

18: toogle advanced

关键知识点

视频演示

18.mp4 (858.83KB)

源文件

18 Toggle-Advanced.framerx.zip

代码展示

import * as React from "react"
import { Frame, Override, Data, useCycle } from "framer"

const appState = Data({
    rotated: false,
})

export function Button(): Override {
    // Create a hook to toggle between the variants
    const [currentVariant, cycleVariant] = useCycle("default", "rotate")

    // Animation variants for the button
    const buttonVariants = {
        default: {
            rotate: 0,
            scale: 1,
        },
        rotate: {
            rotate: 45,
            scale: 0.7,
        },
    }

    return {
        animate: currentVariant,
        variants: buttonVariants,
        onTap() {
            // Toggle between the variants
            cycleVariant()
            // Store the toggled state in the Data object
            appState.rotated = !appState.rotated
        },
    }
}

export function MenuItemOne(props): Override {
    // Animation variants for the menu item
    const menuItemVariants = {
        default: { y: 0, opacity: 0 },
        rotate: { y: -64, opacity: 1 },
    }

    return {
        animate: appState.rotated ? "rotate" : "default",
        variants: menuItemVariants,
        transition: { ease: "easeInOut", duration: 0.2 },
    }
}

export function MenuItemTwo(props): Override {
    // Animation variants for the menu item
    const menuItemVariants = {
        default: { y: 0, opacity: 0 },
        rotate: { y: -134, opacity: 1 },
    }

    return {
        animate: appState.rotated ? "rotate" : "default",
        variants: menuItemVariants,
        transition: { ease: "easeInOut", duration: 0.2 },
    }
}

export function MenuItemThree(props): Override {
    // Animation variants for the menu item
    const menuItemVariants = {
        default: { y: 0, opacity: 0 },
        rotate: { y: -204, opacity: 1 },
    }

    return {
        animate: appState.rotated ? "rotate" : "default",
        variants: menuItemVariants,
        transition: { ease: "easeInOut", duration: 0.2 },
    }
}