Skip to content

Commit f77c485

Browse files
authored
Add tween script and example (#70)
* Add tween script and example * Add tween example to the browser * Fix TS compilation error * Lint fixes * Change example name * Tweak example
1 parent 917f0fe commit f77c485

File tree

10 files changed

+430
-22
lines changed

10 files changed

+430
-22
lines changed

examples/assets/models/star.glb

175 KB
Binary file not shown.

examples/assets/models/star.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Cute little Star by totomori on Sketchfab:
2+
3+
https://sketchfab.com/3d-models/cute-little-star-1fc3bdccaad9455db5a9ed80f5a61cb9
4+
5+
CC BY 4.0 https://creativecommons.org/licenses/by/4.0/

examples/js/examples.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ export const examples = [
1111
{ name: 'Sound', path: 'sound.html' },
1212
{ name: 'Text Elements', path: 'text.html' },
1313
{ name: 'Text 3D', path: 'text3d.html' },
14+
{ name: 'Tweening', path: 'tween.html' },
1415
{ name: 'Video Texture', path: 'video-texture.html' }
1516
];

examples/scripts/tweener.mjs

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import { Tween, Easing } from '@tweenjs/tween.js';
2+
import { Material, Script, Vec2, Vec3, Vec4, Color } from 'playcanvas';
3+
4+
/** @enum {enum} */
5+
const EasingTypes = {
6+
In: 'In',
7+
Out: 'Out',
8+
InOut: 'InOut'
9+
};
10+
11+
/** @enum {enum} */
12+
const EasingFunctions = {
13+
Linear: 'Linear',
14+
Quadratic: 'Quadratic',
15+
Cubic: 'Cubic',
16+
Quartic: 'Quartic',
17+
Quintic: 'Quintic',
18+
Sinusoidal: 'Sinusoidal',
19+
Exponential: 'Exponential',
20+
Circular: 'Circular',
21+
Elastic: 'Elastic',
22+
Back: 'Back',
23+
Bounce: 'Bounce'
24+
};
25+
26+
/** @interface */
27+
class TweenDescriptor { /* eslint-disable-line no-unused-vars */
28+
/**
29+
* Path to the property to tween
30+
* @type {string}
31+
* @attribute
32+
*/
33+
path;
34+
35+
/**
36+
* Start value for the tween
37+
* @type {Vec4}
38+
* @attribute
39+
*/
40+
start;
41+
42+
/**
43+
* End value for the tween
44+
* @type {Vec4}
45+
* @attribute
46+
*/
47+
end;
48+
49+
/**
50+
* Duration of the tween in milliseconds
51+
* @type {number}
52+
* @attribute
53+
*/
54+
duration;
55+
56+
/**
57+
* Delay before starting the tween in milliseconds
58+
* @type {number}
59+
* @attribute
60+
*/
61+
delay;
62+
63+
/**
64+
* Number of times to repeat the tween
65+
* @type {number}
66+
* @attribute
67+
*/
68+
repeat;
69+
70+
/**
71+
* Delay between repeats in milliseconds
72+
* @type {number}
73+
* @attribute
74+
*/
75+
repeatDelay;
76+
77+
/**
78+
* Whether to reverse the tween on repeat
79+
* @type {boolean}
80+
* @attribute
81+
*/
82+
yoyo;
83+
84+
/**
85+
* Index of the easing function to use
86+
* @type {EasingFunctions}
87+
* @attribute
88+
*/
89+
easingFunction = EasingFunctions.Linear;
90+
91+
/**
92+
* Index of the easing type to use
93+
* @type {EasingTypes}
94+
* @attribute
95+
*/
96+
easingType = EasingTypes.InOut;
97+
98+
/**
99+
* Event to fire when tween starts
100+
* @type {string}
101+
* @attribute
102+
*/
103+
startEvent;
104+
105+
/**
106+
* Event to fire when tween stops
107+
* @type {string}
108+
* @attribute
109+
*/
110+
stopEvent;
111+
112+
/**
113+
* Event to fire when tween updates
114+
* @type {string}
115+
* @attribute
116+
*/
117+
updateEvent;
118+
119+
/**
120+
* Event to fire when tween completes
121+
* @type {string}
122+
* @attribute
123+
*/
124+
completeEvent;
125+
126+
/**
127+
* Event to fire when tween repeats
128+
* @type {string}
129+
* @attribute
130+
*/
131+
repeatEvent;
132+
}
133+
134+
export class Tweener extends Script {
135+
/**
136+
* Array of tween configurations
137+
* @type {TweenDescriptor[]}
138+
* @attribute
139+
*/
140+
tweens = [];
141+
142+
/**
143+
* Array of active tween instances
144+
* @type {Tween[]}
145+
*/
146+
tweenInstances = [];
147+
148+
getEasingFunction(tween) {
149+
if (tween.easingFunction === 'Linear') {
150+
return Easing.Linear.None;
151+
}
152+
return Easing[tween.easingFunction][tween.easingType];
153+
}
154+
155+
createStartEndValues(property, start, end) {
156+
if (typeof property === 'number') {
157+
return [{ x: start.x }, { x: end.x }];
158+
}
159+
160+
if (property instanceof Vec2) {
161+
return [new Vec2(start.x, start.y), new Vec2(end.x, end.y)];
162+
}
163+
164+
if (property instanceof Vec3) {
165+
return [new Vec3(start.x, start.y, start.z), new Vec3(end.x, end.y, end.z)];
166+
}
167+
168+
if (property instanceof Vec4) {
169+
return [start.clone(), end.clone()];
170+
}
171+
172+
if (property instanceof Color) {
173+
return [
174+
new Color(start.x, start.y, start.z, start.w),
175+
new Color(end.x, end.y, end.z, end.w)
176+
];
177+
}
178+
179+
console.error('ERROR: tween - specified property must be a number, vec2, vec3, vec4 or color');
180+
return [null, null];
181+
}
182+
183+
handleSpecialProperties(propertyName, propertyOwner, value) {
184+
switch (propertyName) {
185+
case 'localPosition':
186+
propertyOwner.setLocalPosition(value);
187+
break;
188+
case 'localEulerAngles':
189+
propertyOwner.setLocalEulerAngles(value);
190+
break;
191+
case 'localScale':
192+
propertyOwner.setLocalScale(value);
193+
break;
194+
case 'position':
195+
propertyOwner.setPosition(value);
196+
break;
197+
case 'eulerAngles':
198+
propertyOwner.setEulerAngles(value);
199+
break;
200+
}
201+
return null;
202+
}
203+
204+
play(idx) {
205+
const tween = this.tweens[idx];
206+
if (!tween) return;
207+
208+
// Stop any tweens that are animating the same property
209+
this.tweenInstances.forEach((existingTween, i) => {
210+
if (existingTween && this.tweens[i].path === tween.path) {
211+
this.stop(i);
212+
}
213+
});
214+
215+
// Get property owner and name from path
216+
const pathSegments = tween.path.split('.');
217+
let propertyOwner = this.entity;
218+
for (let i = 0; i < pathSegments.length - 1; i++) {
219+
propertyOwner = propertyOwner[pathSegments[i]];
220+
}
221+
222+
const propertyName = pathSegments[pathSegments.length - 1];
223+
const property = propertyOwner[propertyName];
224+
const isNumber = typeof property === 'number';
225+
226+
// Create start and end values
227+
let [startValue, endValue] = this.createStartEndValues(property, tween.start, tween.end);
228+
if (!startValue) return;
229+
230+
// Set initial value
231+
propertyOwner[propertyName] = isNumber ? startValue.x : startValue;
232+
233+
// Handle special properties
234+
const specialValues = this.handleSpecialProperties(propertyName, propertyOwner, startValue);
235+
if (specialValues) {
236+
[startValue, endValue] = specialValues;
237+
}
238+
239+
// Update material if needed
240+
if (propertyOwner instanceof Material) {
241+
propertyOwner.update();
242+
}
243+
244+
// Create and start the tween
245+
this.tweenInstances[idx] = new Tween(startValue)
246+
.to(endValue, tween.duration)
247+
.easing(this.getEasingFunction(tween))
248+
.onStart(() => {
249+
if (tween.startEvent) {
250+
this.app.fire(tween.startEvent);
251+
}
252+
})
253+
.onStop(() => {
254+
if (tween.stopEvent) {
255+
this.app.fire(tween.stopEvent);
256+
}
257+
this.tweenInstances[idx] = null;
258+
})
259+
.onUpdate((obj) => {
260+
propertyOwner[propertyName] = isNumber ? obj.x : obj;
261+
this.handleSpecialProperties(propertyName, propertyOwner, obj);
262+
263+
if (propertyOwner instanceof Material) {
264+
propertyOwner.update();
265+
}
266+
267+
if (tween.updateEvent) {
268+
this.app.fire(tween.updateEvent);
269+
}
270+
})
271+
.onComplete(() => {
272+
if (tween.completeEvent) {
273+
this.app.fire(tween.completeEvent);
274+
}
275+
this.tweenInstances[idx] = null;
276+
})
277+
.onRepeat(() => {
278+
if (tween.repeatEvent) {
279+
this.app.fire(tween.repeatEvent);
280+
}
281+
})
282+
.repeat(tween.repeat)
283+
.repeatDelay(tween.repeatDelay)
284+
.yoyo(tween.yoyo)
285+
.delay(tween.delay)
286+
.start();
287+
}
288+
289+
stop(idx) {
290+
this.tweenInstances[idx]?.stop();
291+
this.tweenInstances[idx] = null;
292+
}
293+
294+
update(dt) {
295+
this.tweenInstances.forEach((tween) => {
296+
tween?.update();
297+
});
298+
}
299+
}

examples/tween.html

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6+
<title>PlayCanvas Web Components - Tweening</title>
7+
<script type="importmap">
8+
{
9+
"imports": {
10+
"playcanvas": "../node_modules/playcanvas/build/playcanvas.mjs",
11+
"@tweenjs/tween.js": "../node_modules/@tweenjs/tween.js/dist/tween.esm.js"
12+
}
13+
}
14+
</script>
15+
<script type="module" src="../dist/pwc.mjs"></script>
16+
<link rel="stylesheet" href="css/example.css">
17+
</head>
18+
<body>
19+
<pc-app>
20+
<pc-asset id="tweener" src="scripts/tweener.mjs" preload></pc-asset>
21+
<pc-asset id="studio" src="assets/skies/octagon-lamps-photo-studio-2k.hdr" preload></pc-asset>
22+
<pc-asset id="star" src="assets/models/star.glb" preload></pc-asset>
23+
<!-- Scene -->
24+
<pc-scene></pc-scene>
25+
<!-- Sky -->
26+
<pc-sky asset="studio" type="none" lighting></pc-sky>
27+
<!-- Camera -->
28+
<pc-entity name="camera" position="0 0 3">
29+
<pc-camera clear-color="#8099e6"></pc-camera>
30+
</pc-entity>
31+
<!-- Star -->
32+
<pc-entity name="star"
33+
onpointerenter="this.entity.script.tweener.play(0)"
34+
onpointerleave="this.entity.script.tweener.play(1)"
35+
onpointerdown="this.entity.script.tweener.play(2)">
36+
<pc-model asset="star"></pc-model>
37+
<pc-scripts>
38+
<pc-script name="tweener" attributes='{
39+
"tweens": [
40+
{
41+
"path": "localScale",
42+
"start": [1, 1, 1, 0],
43+
"end": [1.2, 1.2, 1.2, 0],
44+
"duration": 500,
45+
"easingFunction": "Elastic",
46+
"easingType": "Out"
47+
},
48+
{
49+
"path": "localScale",
50+
"start": [1.2, 1.2, 1.2, 0],
51+
"end": [1, 1, 1, 0],
52+
"duration": 500,
53+
"easingFunction": "Elastic",
54+
"easingType": "Out"
55+
},
56+
{
57+
"path": "localEulerAngles",
58+
"start": [0, 0, 0, 0],
59+
"end": [0, 360, 0, 0],
60+
"duration": 2000,
61+
"easingFunction": "Elastic",
62+
"easingType": "Out"
63+
}
64+
]
65+
}'></pc-script>
66+
</pc-scripts>
67+
</pc-entity>
68+
</pc-scene>
69+
</pc-app>
70+
<script type="module" src="scripts/ui.mjs"></script>
71+
</body>
72+
</html>

0 commit comments

Comments
 (0)