Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multi variant #89

Merged
merged 10 commits into from
Mar 4, 2025
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vue-motion",
"version": "0.10.1",
"version": "0.11.0-beta.6",
"private": true,
"packageManager": "pnpm@9.15.0+sha1.8bfdb6d72b4d5fdf87d21d27f2bfbe2b21dd2629",
"description": "",
Expand Down
2 changes: 1 addition & 1 deletion packages/motion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "motion-v",
"version": "0.10.1",
"version": "0.11.0-beta.6",
"description": "",
"author": "",
"license": "MIT",
Expand Down
3 changes: 3 additions & 0 deletions packages/motion/src/animation/hooks/animation-controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export function setValues(
if (typeof definition === 'string') {
return setVariants(state, [definition])
}
else if (Array.isArray(definition)) {
return setVariants(state, definition)
}
else {
setStateTarget(state, definition)
setTarget(state.visualElement, definition as any)
Expand Down
24 changes: 24 additions & 0 deletions packages/motion/src/components/__tests__/animate-prop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import { Motion } from '@/components'
import { motionValue } from 'framer-motion/dom'
import { computed, nextTick, ref } from 'vue'
import { delay } from '@/shared/test'

function createRerender(Component: any) {
let wrapper: any = null
Expand Down Expand Up @@ -184,4 +185,27 @@ describe('animate prop as object', () => {

await expect(promise).resolves.toBe('1')
})

it('animates through variant array', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const y = motionValue(0)
const { wrapper } = createRerender(Motion)
wrapper.setProps({
animate: ['default', 'open'],
variants: {
default: { x: 10 },
open: { y: 20 },
},
style: { x, y },
})
delay(300).then(() => {
resolve({ x: x.get(), y: y.get() })
})
})

const result: { x: number, y: number } = await promise as any
expect(result.x).toBe(10)
expect(result.y).toBe(20)
})
})
8 changes: 3 additions & 5 deletions packages/motion/src/components/motion/Motion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { Primitive } from './Primitive'
import { MotionState } from '@/state/motion-state'
import { injectAnimatePresence } from '../presence'
import { isMotionValue } from '@/utils'
import { checkMotionIsHidden, getMotionElement } from './utils'
import { checkMotionIsHidden } from './utils'
import type { ElementType, Options, SVGAttributesWithMotionValues, SetMotionValueType } from '@/types'
import { useMotionConfig } from '../motion-config/context'
import { getMotionElement } from '../hooks/use-motion-elm'
</script>

<script setup lang="ts" generic="T extends ElementType = 'div', K = unknown">
Expand Down Expand Up @@ -122,13 +123,10 @@ function getProps() {
...(isSVG ? {} : state.visualElement.latestValues),
}
if (isSVG) {
const { attributes, style } = convertSvgStyleToAttributes(state.target)
const { attributes, style } = convertSvgStyleToAttributes(state.isMounted() ? state.target : state.baseTarget)
Object.assign(attrsProps, attributes)
Object.assign(styleProps, style)
}
if (!state.isMounted()) {
Object.assign(styleProps, state.baseTarget)
}
if (props.drag && props.dragListener !== false) {
Object.assign(styleProps, {
userSelect: 'none',
Expand Down
8 changes: 1 addition & 7 deletions packages/motion/src/components/motion/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { getMotionElement } from '@/components/hooks/use-motion-elm'
import type { ComponentPublicInstance } from 'vue'

export function getMotionElement(el: HTMLElement) {
if (el?.nodeType === 3 || el?.nodeType === 8)
return getMotionElement(el.nextSibling as HTMLElement)

return el
}

/**
* 检查是否是隐藏的 motion 元素
* @param instance
Expand Down
4 changes: 2 additions & 2 deletions packages/motion/src/features/gestures/drag/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DragControls } from '@/features/gestures/drag/use-drag-controls'
import type { Variant } from '@/types'
import type { Variant, VariantLabels } from '@/types'
import type { Axis, BoundingBox, DragElastic, InertiaOptions, PanInfo } from 'framer-motion'

export interface ResolvedConstraints {
Expand Down Expand Up @@ -236,5 +236,5 @@ export interface DragProps extends DragHandlers {
* ```
*/
dragControls?: DragControls
whileDrag?: string | Variant
whileDrag?: VariantLabels | Variant
}
4 changes: 2 additions & 2 deletions packages/motion/src/features/gestures/focus/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Variant } from '@/types'
import type { Variant, VariantLabels } from '@/types'

export type FocusProps = {
focus?: string | Variant
focus?: VariantLabels | Variant
onFocus?: (e: FocusEvent) => void
onBlur?: (e: FocusEvent) => void
}
4 changes: 2 additions & 2 deletions packages/motion/src/features/gestures/hover/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Variant } from '@/types'
import type { Variant, VariantLabels } from '@/types'
import type { EventInfo } from 'framer-motion'

export type HoverEvent = (event: MouseEvent, info: EventInfo) => void

export interface HoverProps {
hover?: string | Variant
hover?: VariantLabels | Variant

onHoverStart?: HoverEvent
onHoverEnd?: HoverEvent
Expand Down
4 changes: 2 additions & 2 deletions packages/motion/src/features/gestures/in-view/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Variant } from '@/types'
import type { Variant, VariantLabels } from '@/types'

type MarginValue = `${number}${'px' | '%'}`

Expand All @@ -14,7 +14,7 @@ type ViewportEventHandler = (entry: IntersectionObserverEntry | null) => void

export interface InViewProps {
inViewOptions?: InViewOptions & { once?: boolean }
inView?: string | Variant
inView?: VariantLabels | Variant

onViewportEnter?: ViewportEventHandler
onViewportLeave?: ViewportEventHandler
Expand Down
4 changes: 2 additions & 2 deletions packages/motion/src/features/gestures/press/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Variant } from '@/types'
import type { Variant, VariantLabels } from '@/types'
import type { EventInfo } from 'framer-motion'

export type PressEvent = (
Expand All @@ -11,7 +11,7 @@ export interface PressProps {
* If `true`, the press gesture will attach its start listener to window.
*/
globalPressTarget?: boolean
press?: string | Variant
press?: VariantLabels | Variant
onPressStart?: PressEvent
onPress?: PressEvent
onPressCancel?: PressEvent
Expand Down
1 change: 1 addition & 0 deletions packages/motion/src/features/layout/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class LayoutFeature extends Feature {
}

// Check lead's animation progress, if it exists, skip update to prevent lead from jumping
// @ts-ignore
if (projection.getStack()?.lead?.animationProgress) {
return
}
Expand Down
Empty file.
30 changes: 17 additions & 13 deletions packages/motion/src/state/animate-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ export function animateUpdates(
) {
const prevTarget = this.target
this.target = { ...this.baseTarget }
const animationOptions: Record<string, $Transition> = {}
let animationOptions: Record<string, $Transition> = {}
const transition = { ...this.options.transition }
// 处理直接动画或状态动画
if (directAnimate)
resolveDirectAnimation.call(this, directAnimate, directTransition, animationOptions)
animationOptions = resolveDirectAnimation.call(this, directAnimate, directTransition, animationOptions)
else
resolveStateAnimation.call(this, controlActiveState, animationOptions)
animationOptions = resolveStateAnimation.call(this, controlActiveState, transition)
const factories = createAnimationFactories.call(this, prevTarget, animationOptions, controlDelay)
const { getChildAnimations, childAnimations } = setupChildAnimations.call(this, transition, controlActiveState, isFallback)
const { getChildAnimations, childAnimations } = setupChildAnimations.call(this, transition, this.activeStates, isFallback)

return executeAnimations.call(this, factories, getChildAnimations, childAnimations, transition, controlActiveState)
}
Expand All @@ -58,20 +58,20 @@ function resolveDirectAnimation(
this: MotionState,
directAnimate: Options['animate'],
directTransition: $Transition | undefined,
animationOptions: Record<string, $Transition>,
) {
const variant = resolveVariant(directAnimate, this.options.variants, this.options.custom)
if (!variant)
return
return {}

const transition = { ...this.options.transition, ...(directTransition || variant.transition) }

const animationOptions = {}
Object.entries(variant).forEach(([key, value]) => {
if (key === 'transition')
return
this.target[key] = value
animationOptions[key] = getOptions(transition, key)
})
return animationOptions
}

/**
Expand All @@ -80,11 +80,12 @@ function resolveDirectAnimation(
function resolveStateAnimation(
this: MotionState,
controlActiveState: Partial<Record<string, boolean>> | undefined,
animationOptions: Record<string, $Transition>,
transition: $Transition,
) {
if (controlActiveState)
this.activeStates = { ...this.activeStates, ...controlActiveState }

const transitionOptions = {}
STATE_TYPES.forEach((name) => {
if (!this.activeStates[name] || isAnimationControls(this.options[name]))
return
Expand All @@ -94,14 +95,15 @@ function resolveStateAnimation(
if (!variant)
return

const transition = { ...this.options.transition, ...variant.transition }
Object.assign(transition, variant.transition)
Object.entries(variant).forEach(([key, value]) => {
if (key === 'transition')
return
this.target[key] = value
animationOptions[key] = getOptions(transition, key)
transitionOptions[key] = getOptions(transition, key)
})
})
return transitionOptions
}

/**
Expand Down Expand Up @@ -145,7 +147,7 @@ function setupChildAnimations(
controlActiveState: Partial<Record<string, boolean>> | undefined,
isFallback: boolean,
) {
if (!this.visualElement.variantChildren?.size || controlActiveState)
if (!this.visualElement.variantChildren?.size || !controlActiveState)
return { getChildAnimations: () => Promise.resolve(), childAnimations: [] }

const { staggerChildren = 0, staggerDirection = 1, delayChildren = 0 } = transition || {}
Expand All @@ -158,14 +160,16 @@ function setupChildAnimations(
.map((child: VisualElement & { state: MotionState }, index) => {
const childDelay = delayChildren + generateStaggerDuration(index)
return child.state.animateUpdates({
controlActiveState: this.activeStates,
controlActiveState,
controlDelay: isFallback ? 0 : childDelay,
})
})
.filter(Boolean) as (() => Promise<any>)[]

return {
getChildAnimations: () => Promise.all(childAnimations.map(animation => animation())),
getChildAnimations: () => Promise.all(childAnimations.map((animation) => {
return typeof animation === 'function' ? animation() : animation
})),
childAnimations,
}
}
Expand Down
17 changes: 8 additions & 9 deletions packages/motion/src/state/motion-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createVisualElement } from '@/state/create-visual-element'
import { doneCallbacks } from '@/components/presence'
import type { StateType } from './animate-updates'
import { animateUpdates } from './animate-updates'
import { isVariantLabels } from '@/state/utils/is-variant-labels'

// Map to track mounted motion states by element
export const mountedStates = new WeakMap<Element, MotionState>()
Expand Down Expand Up @@ -60,6 +61,9 @@ export class MotionState {
// Calculate depth in component tree
this.depth = parent?.depth + 1 || 0

// Initialize with either initial or animate variant
const initialVariantSource = this.context.initial === false ? 'animate' : 'initial'
this.initTarget(initialVariantSource)
// Create visual element with initial config
this.visualElement = createVisualElement(this.options.as!, {
presenceContext: null,
Expand All @@ -78,14 +82,13 @@ export class MotionState {
vars: {},
attrs: {},
},
latestValues: {},
latestValues: {
...this.baseTarget,
},
},
reducedMotionConfig: options.motionConfig.reduceMotion,
})

// Initialize with either initial or animate variant
const initialVariantSource = options.initial === false ? 'animate' : 'initial'
this.initTarget(initialVariantSource)
this.featureManager = new FeatureManager(this)
}

Expand All @@ -96,7 +99,7 @@ export class MotionState {
if (!this._context) {
const handler = {
get: (target: MotionStateContext, prop: keyof MotionStateContext) => {
return typeof this.options[prop] === 'string'
return isVariantLabels(this.options[prop])
? this.options[prop]
: this.parent?.context[prop]
},
Expand All @@ -110,10 +113,6 @@ export class MotionState {
// Initialize animation target values
private initTarget(initialVariantSource: string) {
this.baseTarget = resolveVariant(this.options[initialVariantSource] || this.context[initialVariantSource], this.options.variants) || {}
for (const key in this.baseTarget) {
this.visualElement.setStaticValue(key, this.baseTarget[key])
}

this.target = { }
}

Expand Down
8 changes: 7 additions & 1 deletion packages/motion/src/state/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ export function resolveVariant(
variants?: Options['variants'],
custom?: Options['custom'],
): Variant | undefined {
if (typeof definition === 'object') {
if (Array.isArray(definition)) {
return definition.reduce((acc, item) => {
const resolvedVariant = resolveVariant(item, variants, custom)
return resolvedVariant ? { ...acc, ...resolvedVariant } : acc
}, {})
}
else if (typeof definition === 'object') {
return definition
}
else if (definition && variants) {
Expand Down
5 changes: 5 additions & 0 deletions packages/motion/src/state/utils/is-variant-labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { VariantLabels } from '@/types'

export function isVariantLabels(value: any): value is VariantLabels {
return typeof value === 'string' || value === false || Array.isArray(value)
}
1 change: 1 addition & 0 deletions packages/motion/src/types/framer-motion.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Inertia, Keyframes, None, Repeat, Spring, Tween } from 'framer-motion'

export type { Point } from 'framer-motion'
export interface FrameData {
delta: number
timestamp: number
Expand Down
Loading