PFTween is a Spark AR library for tweening animation.
You can use the similar syntax to DOTween to create animation with JavaScript/TypeScript in Spark AR.
- Install
- Usage
- Getting Started
- Reuse the Animation
- Play Animations in Sequence
- Play Animation with Progress
- Stop Animation
- Donations
You can download script and import it into your Spark AR project, or use this with npm.
-
Drag/Import it into your project. (Spark AR support TypeScript since v105)
-
Import
Ease
andPFTween
module at the top of your script.import { Ease, PFTween } from './PFTween';
-
You can also Click Here to Download Sample Project (v118).
There are four ways to create animation with PFTween.
Create and use animation at once. Learn more
plane0.transform.x = new PFTween(-0.2, 0.2, 1000).scalar;
Create and reuse/control it latter. Learn more
const animation = new PFTween(-0.2, 0.2, 1000)
.onStart(v => plane0.transform.x = v.scalar)
.build(false);
animation.replay();
Create animation and you can await the them to complete. Learn more
const clip = new PFTween(-0.2, 0.2, 1000).clip;
Diagnostics.log('start');
await clip();
Diagnostics.log('complete');
Create then play tweens with progress you like. Learn more
const animation = new PFTween(0, 6, 1000).progress;
progress.setProgress(0) // 0
progress.setProgress(0.5) // 3
progress.setProgress(1) // 6
Let's create an animation, the value is from 0
to 1
in 1000
milliseconds, and output type is ScalarSignal
.
new PFTween(0, 1, 1000).scalar;
You can set it to other ScalarSignal
. E.g. position x, material's opacity, send to PatchEditor, etc.
const plane0 = await Scene.root.findFirst('plane0');
plane0.transform.x = new PFTween(0, 1, 1000).scalar;
You can also set the output to more value type as needed: .scalar
, .pack2
, .pack3
, .pack4
, .deg2rad
, .swizzle()
, .rgba
, .patch()
.
plane0.transform.scale = new PFTween(0, 1, 1000).pack3;
plane0.transform.rotationZ = new PFTween(0, 360, 1000).deg2rad;
plane0.transform.position = new PFTween(-1, 1, 1000).swizzle('xx0');
The default movement is linear, you can change it by chain setEase()
function.
new PFTween(0, 1, 1000)
.setEase(Ease.easeInOutSine) // Remeber to import Ease
.scalar;
And you can add more function to modify this animation. E.g. Make it mirror loop 10 times.
new PFTween(0, 1, 1000)
.setLoops(10)
.setMirror()
.setEase(Ease.easeInOutSine)
.scalar;
There are some events in animation, you can add callback to them using the function named onXXX
.
new PFTween(0, 1, 1000)
.onStart(tweener => {}) // When start, with tweener
.onComplete(() => {) // When animation stop
.onLoop(iteration => {}) // When loop, with iteration
.onUpdate(value => {}) // When tween value changed, with number or number[]
There are also some useful function that can save you time.
const plane0 = await Scene.root.findFirst('plane0');
const material0 = await Materials.findFirst('material0');
new PFTween(0, 1, 1000)
.setDelay(1000) // Delay 1000 milliseconds to start
.onStartVisible(plane0)
.onStartHidden(plane0)
.onCompleteVisible(plane0)
.onCompleteHidden(plane0)
.onCompleteResetPosition(plane0)
.onCompleteResetRotation(plane0)
.onCompleteResetScale(plane0)
.onCompleteResetOpacity(material0)
.onAnimatingVisibleOnly(plane0)
.build()
The from and to can be number
or number[]
. When you use number[]
make sure the two array have the same length.
new PFTween([0, 0], [1, 2], 1000); // O
new PFTween([0, 0, 0], [1, 2], 1000); // X
Notice that the output of number
and number[]
are somewhat different.
new PFTween([0, 0], [1, 2], 1000).scalar; // final: 1
new PFTween([0, 0], [1, 2], 1000).pack2; // final: {x:1 ,y:2}
new PFTween([0, 0], [1, 2], 1000).pack3; // final: {x:1 ,y:2, z:0}
new PFTween(0, 1, 1000).scalar; // final: 1
new PFTween(0, 1, 1000).pack2; // final: {x:1 ,y:1}
new PFTween(0, 1, 1000).pack3; // final: {x:1 ,y:1, z:1}
You can also pass the ScalarSignal
, Point2DSignal
, PointSignal
, Point4DSignal
. These values will be converted to number
or number[]
when you create animation.
new PFTween(plane0.transform.x, 1, 1000);
new PFTween(plane0.transform.scale, [0, 0, 0], 1000);
Everytime you call new PFTween()
will create a new animation object. Sometimes, it's not neccesary to create a new animation, you can reuse it for better performance. (However, in generally, user don't notice the performance impact as well)
E.g., you need to punch a image every time user open their mouth:
const onMouthOpen = FaceTracking.face(0).mouth.openness.gt(0.2).onOn();
onMouthOpen.subscribe(play_punch_animation);
function play_punch_animation(){
plane0.transform.scale = new PFTween(1, 0.3, 1000).setEase(Ease.punch).pack3;
}
It works, but you don't need to create a new animation every time you play.
Use onStart()
to set the value and call build()
at the end of PFTween
chain. It will return a PFTweener
, a controller for PFTween
object. You can call replay
, reverse
, start
, stop
or get isRunning
.
const onMouthOpen = FaceTracking.face(0).mouth.openness.gt(0.2).onOn();
const play_punch_animation = new PFTween(1, 0.3, 1000)
.setEase(Ease.punch)
.onStart(tweener => plane0.transform.scale = tweener.pack3)
.build(false); // The 'false' means don't play animation when build. Default is 'true'.
onMouthOpen.subscribe(() => play_punch_animation.replay());
PFTweener
is actually a wrapped AnimationModule.TimeDriver
, so you can find the similar APIs from the official document.
.clip
is an asynchronous way to reuse animation based on Promise
. With clip
, you can play tween animation in sequence.
E.g., jump().then(scale).then(rotate).then(fadeout).then(......
In order to use clip
, you must set the value with onStart()
, and get clip
instead of call build()
at the end of PFTween
chain.
When you get clip
, it returns a Promise. If you want to play the clip, just call clip()
.
const clip1 = new PFTween(0, 1, 500).clip;
const clip2 = new PFTween(1, 2, 500).clip;
const clip3 = new PFTween(2, 3, 500).clip;
clip1().then(clip2).then(clip3);
In addition to manually play multiple clips using then()
, you can also use PFTween.concat()
to concatenate them into one clip
.
const clip1 = new PFTween(0, 1, 500).clip;
const clip2 = new PFTween(1, 2, 500).clip;
const clip3 = new PFTween(2, 3, 500).clip;
const animations = PFTween.concat(clip1, clip2, clip3);
animations();
If you want to start multiple clips at the same time, you can use PFTween.combine()
to combine multiple clips in to one clip
.
const clip1 = new PFTween(0, 1, 500).clip;
const clip2 = new PFTween(1, 2, 500).clip;
const clip3 = new PFTween(2, 3, 500).clip;
const animations = PFTween.combine(clip1, clip2, clip3);
animations();
.progress
is based on Animation.ValueDriver
, you can control it with progress you like. The progress value is clamped in 0-1.
The onComplete
, onStart
, onLoop
and their related won't work, so you have to use onUpdate()
to set values.
const animation = new PFTween(-0.1, 0.1, 500).onUpdate(v => plane0.transform.x = v).progress;
animation.setProgress(0); // plane0.transform.x = -0.1
animation.setProgress(0.5); // plane0.transform.x = 0
animation.setProgress(1); // plane0.transform.x = 0.1
// or you can pass a ScalarSignal
animation.setProgress(new PFTween(0, 1, 1000).scalar);
You can use combineProgress
and concatProgress
to merge multiple progress.
import { PFTween } from './PFTween';
import Scene from 'Scene';
import Diagnostics from 'Diagnostics';
(async () => {
const plane0 = await Scene.root.findFirst('plane0');
const p1 = new PFTween(0, 0.2, 500).onUpdate(v => plane0.transform.x = v).progress;
const p2 = new PFTween(0, 0.1, 500).onUpdate(v => plane0.transform.y = v).progress;
const p3 = new PFTween(0.2, 0, 500).onUpdate(v => plane0.transform.x = v).progress;
// The "combineProgress" and "concatProgress" are static functions
const combine = PFTween.combineProgress(p1, p2);
const animation = PFTween.concatProgress(combine, p3);
})();
There are three ways to create animation with PFTween.
If your animation is made with .build()
, it's will return a controller. You can stop the animation with controller's stop()
function.
import { PFTween } from './PFTween';
import Scene from 'Scene';
import TouchGestures from 'TouchGestures';
(async () => {
const plane0 = await Scene.root.findFirst('plane0');
const controller = new PFTween(0, 1, 1000)
.setLoops(true)
.setId('foo')
.onStart(v => plane0.transform.x = v.scalar)
.build();
TouchGestures.onTap().subscribe(() => {
controller.stop();
});
})();
You can add .setId("id")
to any of your tween, and then use the static function PFTween.kill("id")
to kill and stop the animation. Please note that if you kill the animation, all of the events will be removed. (i.e. The animation you killed can't be reused)
import { PFTween } from './PFTween';
import Scene from 'Scene';
import TouchGestures from 'TouchGestures';
(async () => {
const plane0 = await Scene.root.findFirst('plane0');
plane0.transform.x = new PFTween(0, 1, 1000).setLoops(true).setId('foo').scalar;
TouchGestures.onTap().subscribe(() => PFTween.kill('foo'));
})();
If your animation is created with basic way such .scalar
, .pack2
, .pack3
...... The animation will be auto killed after complete.
If you animation is made with .clip
, you can create a cancellationa and pass it when you play the clip.
import { PFTween } from './PFTween';
import Scene from 'Scene';
import TouchGestures from 'TouchGestures';
(async () => {
const plane0 = await Scene.root.findFirst('plane0');
// PFTween.newCancellation is static function
const cancellation = PFTween.newClipCancellation();
new PFTween(0, 1, 1000)
.setLoops(true)
.onStart(v => plane0.transform.x = v.scalar)
.clip(cancellation);
TouchGestures.onTap().subscribe(() => {
cancellation.cancel();
});
})();
Unlike setId
/kill
, canceled clips can be played again, and all events you added will remain.
If this is useful for you, please consider a donationšš¼. One-time donations can be made with PayPal.