Skip to content

Commit 0844f16

Browse files
committed
feat: add animate in MTS
1 parent bd73b8d commit 0844f16

File tree

6 files changed

+191
-0
lines changed

6 files changed

+191
-0
lines changed

.changeset/weak-animals-search.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@lynx-js/react': patch
3+
---
4+
5+
Add `animate` API in Main Thread Script(MTS), so you can now control a CSS animation imperatively
6+
7+
```
8+
function startAnimation() {
9+
'main thread'
10+
const animation = ele.animate([
11+
{ opacity: 0 },
12+
{ opacity: 1 },
13+
], {
14+
duration: 3000
15+
})
16+
17+
animation.pause()
18+
}
19+
```

packages/react/worklet-runtime/__test__/api/element.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// LICENSE file in the root directory of this source tree.
44
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
55

6+
import { AnimationOperation } from '../../src/api/animation/animation';
67
import { Element } from '../../src/api/element';
78
import { initWorklet } from '../../src/workletRuntime';
89

@@ -21,6 +22,7 @@ beforeEach(() => {
2122
globalThis.__QuerySelectorAll = vi.fn();
2223
globalThis.__InvokeUIMethod = vi.fn();
2324
globalThis.__FlushElementTree = vi.fn();
25+
globalThis.__ElementAnimate = vi.fn();
2426
});
2527

2628
afterEach(() => {
@@ -142,4 +144,51 @@ describe('Element', () => {
142144
await vi.runAllTimersAsync();
143145
expect(globalThis.__FlushElementTree).toHaveBeenCalledTimes(1);
144146
});
147+
148+
it('should start animation when created', () => {
149+
const element = new Element('element-instance');
150+
const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
151+
expect(globalThis.__ElementAnimate).toHaveBeenCalledWith('element-instance', [
152+
AnimationOperation.START,
153+
animation.id,
154+
[{ opacity: 0 }, { opacity: 1 }],
155+
{ duration: 1000 },
156+
]);
157+
});
158+
159+
it('should cancel animation when canceled', () => {
160+
const element = new Element('element-instance');
161+
const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
162+
animation.cancel();
163+
expect(globalThis.__ElementAnimate).toHaveBeenCalledWith('element-instance', [
164+
AnimationOperation.CANCEL,
165+
animation.id,
166+
]);
167+
});
168+
169+
it('should pause animation when paused', () => {
170+
const element = new Element('element-instance');
171+
const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
172+
animation.pause();
173+
expect(globalThis.__ElementAnimate).toHaveBeenCalledWith('element-instance', [
174+
AnimationOperation.PAUSE,
175+
animation.id,
176+
]);
177+
});
178+
179+
it('should play animation when played', () => {
180+
const element = new Element('element-instance');
181+
const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
182+
expect(globalThis.__ElementAnimate).toHaveBeenCalledWith('element-instance', [
183+
AnimationOperation.START,
184+
animation.id,
185+
[{ opacity: 0 }, { opacity: 1 }],
186+
{ duration: 1000 },
187+
]);
188+
animation.play();
189+
expect(globalThis.__ElementAnimate).toHaveBeenCalledWith('element-instance', [
190+
AnimationOperation.PLAY,
191+
animation.id,
192+
]);
193+
});
145194
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2025 The Lynx Authors. All rights reserved.
2+
// Licensed under the Apache License Version 2.0 that can be found in the
3+
// LICENSE file in the root directory of this source tree.
4+
import type { KeyframeEffect } from './effect.js';
5+
6+
export enum AnimationOperation {
7+
START = 0, // Start a new animation
8+
PLAY = 1, // Play/resume a paused animation
9+
PAUSE = 2, // Pause an existing animation
10+
CANCEL = 3, // Cancel an animation
11+
}
12+
13+
export class Animation {
14+
static count = 0;
15+
public readonly effect: KeyframeEffect;
16+
public readonly id: string;
17+
18+
constructor(effect: KeyframeEffect) {
19+
this.effect = effect;
20+
this.id = '__lynx-inner-js-animation-' + Animation.count++;
21+
this.start();
22+
}
23+
24+
public cancel(): void {
25+
// @ts-expect-error expected
26+
return __ElementAnimate(this.effect.target.element, [AnimationOperation.CANCEL, this.id]);
27+
}
28+
29+
public pause(): void {
30+
// @ts-expect-error expected
31+
return __ElementAnimate(this.effect.target.element, [AnimationOperation.PAUSE, this.id]);
32+
}
33+
34+
public play(): void {
35+
// @ts-expect-error expected
36+
return __ElementAnimate(this.effect.target.element, [AnimationOperation.PLAY, this.id]);
37+
}
38+
39+
private start(): void {
40+
// @ts-expect-error expected
41+
return __ElementAnimate(this.effect.target.element, [
42+
AnimationOperation.START,
43+
this.id,
44+
this.effect.keyframes,
45+
this.effect.options,
46+
]);
47+
}
48+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2025 The Lynx Authors. All rights reserved.
2+
// Licensed under the Apache License Version 2.0 that can be found in the
3+
// LICENSE file in the root directory of this source tree.
4+
import type { Element } from '../element.js';
5+
6+
export class KeyframeEffect {
7+
public readonly target: Element;
8+
public readonly keyframes: Record<string, number | string>[];
9+
public readonly options: Record<string, number | string>;
10+
11+
constructor(
12+
target: Element,
13+
keyframes: Record<string, number | string>[],
14+
options: Record<string, number | string>,
15+
) {
16+
this.target = target;
17+
this.keyframes = keyframes;
18+
this.options = options;
19+
}
20+
}

packages/react/worklet-runtime/src/api/element.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright 2024 The Lynx Authors. All rights reserved.
22
// Licensed under the Apache License Version 2.0 that can be found in the
33
// LICENSE file in the root directory of this source tree.
4+
import { Animation } from './animation/animation.js';
5+
import { KeyframeEffect } from './animation/effect.js';
6+
47
export class Element {
58
private static willFlush = false;
69

@@ -54,6 +57,11 @@ export class Element {
5457
});
5558
}
5659

60+
public animate(effect: Record<string, number | string>[], options: Record<string, number | string>): Animation {
61+
const animation = new Animation(new KeyframeEffect(this, effect, options));
62+
return animation;
63+
}
64+
5765
public invoke(
5866
methodName: string,
5967
params?: Record<string, unknown>,

packages/react/worklet-runtime/src/types/elementApi.d.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,50 @@ declare function __QuerySelectorAll(
4646
): ElementNode[];
4747

4848
declare function __SetAttribute(e: ElementNode, key: string, value: unknown): void;
49+
50+
/**
51+
* Animation operation types for ElementAnimate function
52+
*/
53+
enum AnimationOperation {
54+
START = 0, // Start a new animation
55+
PAUSE = 1, // Pause an existing animation
56+
PLAY = 2, // Play/resume a paused animation
57+
CANCEL = 3, // Cancel an animation
58+
}
59+
60+
/**
61+
* Animation timing options configuration
62+
*/
63+
interface AnimationTimingOptions {
64+
name?: string; // Animation name (optional, auto-generated if not provided)
65+
duration?: number | string; // Animation duration
66+
delay?: number | string; // Animation delay
67+
iterationCount?: number | string; // Number of iterations (can be 'infinite')
68+
fillMode?: string; // Animation fill mode
69+
timingFunction?: string; // Animation timing function
70+
direction?: string; // Animation direction
71+
}
72+
73+
/**
74+
* Keyframe definition for animation
75+
*/
76+
type Keyframe = Record<string, string | number>;
77+
78+
/**
79+
* ElementAnimate function - controls animations on DOM elements
80+
* @param element - The DOM element to animate (FiberElement reference)
81+
* @param args - Animation configuration array
82+
* @returns undefined
83+
*/
84+
declare function __ElementAnimate(
85+
element: FiberElement,
86+
args: [
87+
operation: AnimationOperation, // Animation operation type
88+
name: string, // Animation name
89+
keyframes: Keyframe[], // Array of keyframes
90+
options?: AnimationTimingOptions, // Timing and configuration options
91+
] | [
92+
operation: AnimationOperation.PAUSE | AnimationOperation.PLAY | AnimationOperation.CANCEL, // Only PAUSE or PLAY
93+
name: string, // Animation name to pause/play
94+
],
95+
): void;

0 commit comments

Comments
 (0)