import { decorate, useStore } from '@o/use-store' import { MotionValue, useSpring, useTransform, useViewportScroll } from 'framer-motion' import { SpringProps } from 'popmotion' import React, { memo } from 'react' import { RefObject, useCallback, useContext, useEffect, useRef } from 'react' import { useRelative } from './hooks/useRelative' import { useScrollProgress } from './hooks/useScrollProgress' import { useScrollableParent } from './View/ScrollableParentStore' //1 // @ts-ignore @decorate class HooksStore { hooksKey = 0 hooks: ((lastHooks: T[]) => T)[] = [] addHook(next: (lastHooks: T[]) => T) { this.hooks = [...this.hooks, next] this.hooksKey = Math.random() } } export class AnimationStore { animationHooks = new HooksStore() frozen = false values = [] freeze() { this.frozen = true } getValue() { return this.values[this.values.length - 1] } transform(transform: (v: any) => T): AnimationStore transform(from: number[], to?: any[], options?: any): AnimationStore transform(from: any, to?: any[], options?: any): AnimationStore { if (this.frozen) return this this.animationHooks.addHook(hooksValues => { const lastHookVal = hooksValues[hooksValues.length - 1] return useTransform(lastHookVal, from, to, options as any) }) return this } spring(props: SpringProps | false) { if (!props) return this if (this.frozen) return this this.animationHooks.addHook(hooksValues => { const lastHookVal = hooksValues[hooksValues.length - 1] return useSpring(lastHookVal, props) }) return this } mergeTransform(values: MotionValue[], callback: (previous: T, ...values: T[]) => T) { if (this.frozen) return this this.animationHooks.addHook(hooksValues => { const lastHookVal = hooksValues[hooksValues.length - 1] return useRelative(callback, lastHookVal, ...values) }) return this } } export class GeometryStore { stores: AnimationStore[] = [] curCall = 0 frozen = false props: A & { ref: RefObject } beforeRenderChildren() { this.curCall = 0 } afterRenderChildren() { for (const store of this.stores) { const hooks = [] for (const hook of store.animationHooks.hooks) { hooks.push(hook(hooks)) } store.values = hooks store.freeze() } this.freeze() } clear() { this.stores = [] this.curCall = 0 this.frozen = false } freeze() { this.frozen = true } setupStore(fn: (store: AnimationStore) => void) { const curStore = this.stores[this.curCall] this.curCall++ if (curStore) { return curStore } if (this.frozen) { throw new Error(`Error, using Geometry strangely`) } const store = new AnimationStore() fn(store) this.stores = [...this.stores, store] return store } /** * Viewport scroll */ useViewportScroll(key: 'x' | 'y' | 'xProgress' | 'yProgress') { return this.setupStore(store => { store.animationHooks.addHook(() => { return useViewportScroll()[`scroll${key[0].toUpperCase()}${key.slice(1)}`] }) }) } /** * Use an existing MotionValue and transform it */ useTransform(value: MotionValue, transform?: (val: T) => T) { return this.setupStore(store => { store.animationHooks.addHook(() => { return useTransform(value, transform) }) }) } /** * Returns a 0 to 1 value (for any child) representing the current scroll position of the parent scrollable */ scrollProgress(direction: 'x' | 'y') { return this.setupStore(store => { store.animationHooks.addHook(() => { const scrollableParent = useScrollableParent() return useScrollProgress({ ref: scrollableParent.props.ref, direction, }) }) }) } /** * Returns -1 to 1 value of the current nodes intersection within the parent scrollable * (where -1 is off on the left/top, and 1 is off on the right/bottom, 0 is centered) */ scrollIntersection() { return this.setupStore(store => { store.animationHooks.addHook(() => { const scrollableParentStore = useScrollableParent() if (!scrollableParentStore) { throw new Error( 'No scrollable parent, must have a view like as parent', ) } const { scrollIntersectionState } = scrollableParentStore useEffect(() => { scrollableParentStore.startIntersection() }, []) // this should make it go to 0 once node is centered return useTransform(scrollIntersectionState.scrollProgress, scroll => { if (!scrollIntersectionState.ready) { return 0 } const index = scrollIntersectionState.elements.indexOf(this.props.ref.current) const state = scrollIntersectionState.measurements.get(index) const res = state ? (state.offset - scroll) * state.total : 0 // sometimes we'd get incredibly close to accurate but off by just a tiny bit return Math.round(res * 10000) / 10000 }) }) }) } } export type GeometryRenderer = ( geometry: A, ref: RefObject, ) => React.ReactNode export function useGeometry( getChildren: GeometryRenderer, GeometryConstructor: { new (...args: any): A }, geometryProps?: any, ) { const ref = useRef() const geometry = useStore(GeometryConstructor, { ref, ...geometryProps }, { react: false }) geometry.beforeRenderChildren() const children = getChildren(geometry, ref) geometry.afterRenderChildren() return children as JSX.Element } export const Geometry = memo( (props: { children: GeometryRenderer }) => { return useGeometry(props.children, GeometryStore) }, // because it takes a function as children we should never have to update () => false, )