Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions packages/motion/src/api/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
TriggerVariant,
UnitLengthPercentage,
} from '../types';
import { getCssUnits, getEasing, normalizeKeyframes } from '../utils';
import { getCssUnits, getEasing } from '../utils';
import fastdom from 'fastdom';
import { getRegisteredEffect } from './registry';

Expand Down Expand Up @@ -119,12 +119,10 @@ function getEffectsData(
}
}

// keyframe property names may be authored in camelCase or kebab-case,
// WAAPI only understands camelCase
const keyframes = normalizeKeyframes(effect.keyframes);

return {
effect: keyframes === effect.keyframes ? effect : { ...effect, keyframes },
// pass the preset's object as is - its keyframes and ranges may be getters
// that must only be read after its measure() callbacks ran
effect,
options: effectOptions,
id: effectId && `${effectId}-${index + 1}`,
part: effect.part,
Expand Down
4 changes: 2 additions & 2 deletions packages/motion/src/api/cssAnimations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
SequenceOptions,
TriggerVariant,
} from '../types';
import { getJsEasingInCSS } from '../utils';
import { getJsEasingInCSS, normalizeKeyframes } from '../utils';
import { getEffectsData, getRanges, getNamedEffect, isNotAScrubTrigger } from './common';

function getAnimationTarget(target: string | null, part: string | undefined) {
Expand Down Expand Up @@ -96,7 +96,7 @@ function getCSSAnimation(
composition: item.options.composite,
custom: item.effect.custom,
name: item.effect.name,
keyframes: item.effect.keyframes,
keyframes: normalizeKeyframes(item.effect.keyframes),
id: item.id && `${item.id}-${index + 1}`,
animationTimeline: isViewProgress ? `--${trigger?.id}` : '',
animationRange: start || end ? `${start} ${end}` : '',
Expand Down
5 changes: 4 additions & 1 deletion packages/motion/src/api/webAnimations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from '../types';
import { CustomAnimation } from '../CustomAnimation';
import { AnimationGroup } from '../AnimationGroup';
import { normalizeKeyframes } from '../utils';
import {
getElement,
getElementMotionPart,
Expand Down Expand Up @@ -137,7 +138,9 @@ function getWebAnimation(
keyframeEffect.updateTiming(effect.timing as OptionalEffectTiming);
}

keyframeEffect.setKeyframes(effect.keyframes);
// keyframe property names may be authored in camelCase or kebab-case,
// WAAPI only understands camelCase
keyframeEffect.setKeyframes(normalizeKeyframes(effect.keyframes));
});

const timingOptions =
Expand Down
98 changes: 98 additions & 0 deletions packages/motion/test/motion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,104 @@ describe('motion.ts', () => {
(AnimationGroup as Mock).mockRestore();
});

test('should read lazy keyframes after the preset measured the target', async () => {
registerEffects({
LazyKeyframes: {
web: (options: AnimationOptions, dom: any) => {
let offset = 0;
dom.measure(() => {
offset = 0.4;
});

return [
{
...options,
name: 'lazy-keyframes',
get keyframes() {
return [
{ offset: 0, opacity: 0 },
{ offset, opacity: 1 },
];
},
},
];
},
},
} as any);

const fastdom = (await import('fastdom')).default;
// queue fastdom work like a browser frame does, so the measure runs after getWebAnimation() returns
const frame: Array<() => void> = [];
(fastdom.measure as Mock).mockImplementation((fn) => frame.push(fn));
(fastdom.mutate as Mock).mockImplementation((fn) => frame.push(fn));

getWebAnimation(document.createElement('div'), {
namedEffect: { type: 'LazyKeyframes', id: 'lazy' },
duration: 1000,
} as AnimationOptions);
frame.forEach((fn) => fn());

expect(mockKeyframeEffect.setKeyframes).toHaveBeenCalledWith([
{ offset: 0, opacity: 0 },
{ offset: 0.4, opacity: 1 },
]);

(fastdom.measure as Mock).mockRestore();
(fastdom.mutate as Mock).mockRestore();
});

test('should read lazy scrub ranges after the preset measured the target', async () => {
registerEffects({
LazyRange: {
web: (options: AnimationOptions, dom: any) => {
let height = 0;
dom.measure(() => {
height = 200;
});

// like BgZoom and BgFake3D, both the keyframes and the range end are getters
return [
{
...options,
name: 'lazy-range',
get keyframes() {
return [{ opacity: 0 }, { opacity: 1 }];
},
startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } },
endOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } },
get endOffsetAdd() {
return `${height}px`;
},
},
];
},
},
} as any);

const { AnimationGroup } = await import('../src/AnimationGroup');
const fastdom = (await import('fastdom')).default;
const frame: Array<() => void> = [];
(fastdom.measure as Mock).mockImplementation((fn) => frame.push(fn));
(fastdom.mutate as Mock).mockImplementation((fn) => frame.push(fn));

const target = document.createElement('div');
getWebAnimation(
target,
{ namedEffect: { type: 'LazyRange', id: 'lazy' } } as AnimationOptions,
{
trigger: 'view-progress',
element: target,
},
);
frame.forEach((fn) => fn());

const [animation] = (AnimationGroup as Mock).mock.calls[0][0];
expect(animation.rangeEnd).toBe('cover calc(0% + 200px)');

(fastdom.measure as Mock).mockRestore();
(fastdom.mutate as Mock).mockRestore();
});

test('should handle custom ownerDocument', async () => {
const customDocument = {
getElementById: vi.fn(() => mockElement),
Expand Down
Loading