UNPKG

framer-controller

Version:

Control components and state in Framer X with reusable controllers.

443 lines (433 loc) 13.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const React = require("react"); const framer_1 = require("framer"); /** * # createScrollControls * Returns a collection of hooks for using the state of a Scroll component... * - `useScrollConnector` - Connects a Scroll component to the other hooks. * - `useScrollState` - Returns the Scroll's state. * - `useScrollItemState` - Returns scroll information about a child of the Scroll's content. * * ... and also controls for settitng that state. * - `setScrollX` * - `setScrollY`, * - `setScrollPoint`, * - `scrollToPoint`, * ```tsx const { useScrollConnector, useScrollState, useScrollItemState, setScrollX, setScrollY, setScrollPoint, scrollToPoint, } = createScrollControls() ```*/ function createScrollControls(options = {}) { const { scrollY = 0, scrollX = 0 } = options; const contentOffsetX = framer_1.motionValue(-scrollX); const contentOffsetY = framer_1.motionValue(-scrollY); // Initial Store let store = { component: null, content: null, direction: 'vertical', scrollY, scrollX, scrollPoint: { x: scrollX, y: scrollY, }, velocity: { x: 0, y: 0, }, progress: { x: 0, y: 0, }, scrollFrame: { x: 0, y: 0, width: 100, height: 100, }, scrollConstraints: { top: 0, right: 100, bottom: 100, left: 0, }, }; // Set Motion Values const updateMotionValues = (x, y) => { contentOffsetX.stop(); contentOffsetX.set(x); contentOffsetY.stop(); contentOffsetY.set(y); }; // Store const storeSetters = new Set(); const childSetters = new Set(); const setStoreState = (changes, updateMotion) => { store = Object.assign({}, store, changes); store = Object.assign({}, store, { scrollPoint: { x: store.scrollX, y: store.scrollY, }, progress: { x: store.scrollX / (store.scrollConstraints.right - store.scrollConstraints.left) || 0, y: store.scrollY / (store.scrollConstraints.bottom - store.scrollConstraints.top) || 0, } }); if (updateMotion) { updateMotionValues(-store.scrollX, -store.scrollY); } // Update store for useScrollState storeSetters.forEach((setter) => setter(store)); // Update store for children childSetters.forEach((setter) => setter(store)); }; const set = (values, updateMotion = true) => setStoreState(values, updateMotion); // Update from contentOffsetY const updateFromMotionValues = (scrollX, scrollY, delta, offset, velocity) => { set({ scrollX, scrollY, progress: { x: scrollX / (store.scrollConstraints.right - store.scrollConstraints.left) || 0, y: scrollY / (store.scrollConstraints.bottom - store.scrollConstraints.top) || 0, }, delta, offset, velocity, }, false); }; // Scroll events const onScroll = ({ point, delta, offset, velocity }) => { updateFromMotionValues(-point.x, -point.y, delta, offset, velocity); }; // Animation controls let scrollAnimate; /** * # useScrollConnector * A hook for connecting a Scroll component to the other hooks created by `createScrollControls`. * In order for the other hooks to work, this hook must be called from the Scroll component's * override as shown below. * ```tsx // Sync the component with the usePageControls hook export function ScrollComponent(props): Override { const overrides = useScrollConnector(props) return { ...(overrides as any), } } ``` */ const useScrollConnector = (props) => { scrollAnimate = framer_1.useAnimation(); // Set state from props if (props) { // Ensure that the connected props are from a Scroll component const { componentIdentifier } = props; if (!componentIdentifier || componentIdentifier !== 'framer/Scroll') { console.error("⚠️ You've passed the props of some non-Scroll component to useScrollConnector. You should only pass props from the Scroll component that you're trying to control."); return; } const component = props.children[0]; const content = component.props.children[0]; React.useEffect(() => { set({ component, content, scrollConstraints: { top: 0, right: content.props.width - component.props.width, bottom: content.props.height - component.props.height, left: 0, }, scrollFrame: { x: 0, y: 0, width: component.props.width, height: component.props.height, }, }, false); }, [component, component.props, content, content.props.children]); } return { contentOffsetX, contentOffsetY, scrollAnimate, onScroll, }; }; /** * ## setScrollPoinX * Set the Scroll component's scrollX value. * * ```tsx export function SetScrollX500(): Override { const { setScrollX } = useScrollControls() return { onTap: () => setScrollX(500), } } ```*/ const setScrollX = (scrollX) => { set({ scrollX, }); }; /** * ## setScrollY * Set the Scroll component's scrollY value. * * ```tsx export function SetScrollY500(): Override { const { setScrollY } = useScrollControls() return { onTap: () => setScrollY(500), } } ```*/ const setScrollY = (scrollY) => { set({ scrollY, }); }; /** * ## setScrollPoint * Set the Scroll component's scroll point (`x` and `y`). * * ```tsx export function SetScrollPoint200500(): Override { const { setScrollPoint } = useScrollControls() return { onTap: () => setScrollPoint({ x: 200, y: 500 }), } } ```*/ const setScrollPoint = (point) => { set({ scrollX: point.x, scrollY: point.y, }); }; /** * ## ScrollToPoint * Scroll the Scroll component's to a given point ( `x` and `y` ), with an optional `transition`. * * ```tsx // Without transition export function ScrollTo200500(): Override { const { scrollToPoint } = useScrollControls() return { onTap: () => scrollToPoint({ x: 200, y: 500 }), } } // With transition export function ScrollTo200500(): Override { const { scrollToPoint } = useScrollControls() return { onTap: () => scrollToPoint({ x: 200, y: 500, transition: { type: "tween", duration: 2 } }), } } ```*/ const scrollToPoint = (point) => { scrollAnimate.start({ x: -point.x, y: -point.y, transition: point.transition, }); }; /** * # useScrollState * Hook that provides state information about the Scroll component. */ const useScrollState = () => { const [state, setState] = React.useState(store); React.useEffect(() => { storeSetters.add(setState); return () => storeSetters.delete(setState); }, []); return state; }; /** * # useScrollItemState * Hook that provides scroll information for children of the Scroll component's content Frame. * - offset, * - clip, * - intersect, * - travel, * - progress, */ const useScrollItemState = (props) => { const [state, setState] = React.useState({ absolute: { x: 0, y: 0, midX: 50, midY: 50, maxX: 100, maxY: 100, height: 100, width: 100, }, offset: { top: 0, right: 0, bottom: 0, left: 0 }, intersect: { x: 0, y: 0 }, travel: { x: 0, y: 0 }, progress: { x: 0, y: 0 }, clip: { x: 'contain', y: 'contain' }, }); const calculateClip = (axis, a, b, max) => { const labels = axis === 'x' ? ['left', 'right'] : ['top', 'bottom']; if (a < 0) { return b <= 0 ? 'before' : b <= max ? labels[0] : 'overflow'; } else if (b > max) { return a >= max ? 'after' : a >= 0 ? labels[1] : 'overflow'; } else { return 'contain'; } }; const updateStore = (store) => { const { scrollFrame: sf, scrollX, scrollY } = store; setState((state) => { const { absolute } = state; const offset = { top: absolute.y - scrollY, right: absolute.maxX - scrollX, bottom: absolute.maxY - scrollY, left: absolute.x - scrollX, }; const clip = { x: calculateClip('x', offset.left, offset.right, sf.width), y: calculateClip('y', offset.top, offset.bottom, sf.height), }; const intersect = { x: { before: 0, after: 0, right: (sf.width - offset.left) / absolute.width, left: offset.right / absolute.width, overflow: 1, contain: 1, }[clip.x], y: { before: 0, after: 0, bottom: (sf.height - offset.top) / absolute.height, top: offset.bottom / absolute.height, overflow: 1, contain: 1, }[clip.y], }; const travel = { x: { before: 1, after: -1, right: framer_1.transform(offset.left, [sf.width, sf.width - absolute.width], [-1, 0]), left: framer_1.transform(offset.left, [0, -absolute.width], [0, 1]), overflow: 0, contain: 0, }[clip.x], y: { before: 1, after: -1, bottom: framer_1.transform(offset.top, [sf.height, sf.height - absolute.height], [-1, 0]), top: framer_1.transform(offset.top, [0, -absolute.height], [0, 1]), overflow: 0, contain: 0, }[clip.y], }; const progress = { x: framer_1.transform(offset.left, [sf.width, -absolute.width], [0, 1]), y: framer_1.transform(offset.top, [sf.height, -absolute.height], [0, 1]), }; return Object.assign({}, state, { offset, clip, intersect, travel, progress }); }); }; React.useEffect(() => { childSetters.add(updateStore); return () => childSetters.delete(updateStore); }, []); React.useEffect(() => { if (!store.content) return; const isChild = store.content.props.children .map((c) => c.props.id) .includes(props.id); if (!isChild) { console.error("⚠️ You've passed the props of a component that isn't a child of the Scroll component's content Frame to useScrollControls. You should only pass props from a component that is a descendent of the Scroll's connected content Frame."); return; } const absolute = { x: props.left, y: props.top, midX: props.left + props.width / 2, midY: props.top + props.height / 2, maxX: props.left + props.width, maxY: props.top + props.height, height: props.height, width: props.width, }; setState(Object.assign({}, state, { absolute })); }, [store.content]); return state; }; return { useScrollConnector, useScrollState, useScrollItemState, setScrollX, setScrollY, setScrollPoint, scrollToPoint, }; } exports.createScrollControls = createScrollControls; const findChildById = (children, id) => { return children.find((child) => { if (child.props.id === id) { return true; } if (child.props.children) { return findChildById(child.props.children, id); } else { return false; } }) ? true : false; };