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

Add tween script and example #70

Merged
merged 6 commits into from
Dec 8, 2024
Merged
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
Binary file added examples/assets/models/star.glb
Binary file not shown.
5 changes: 5 additions & 0 deletions examples/assets/models/star.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Cute little Star by totomori on Sketchfab:

https://sketchfab.com/3d-models/cute-little-star-1fc3bdccaad9455db5a9ed80f5a61cb9

CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
1 change: 1 addition & 0 deletions examples/js/examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export const examples = [
{ name: 'Sound', path: 'sound.html' },
{ name: 'Text Elements', path: 'text.html' },
{ name: 'Text 3D', path: 'text3d.html' },
{ name: 'Tweening', path: 'tween.html' },
{ name: 'Video Texture', path: 'video-texture.html' }
];
299 changes: 299 additions & 0 deletions examples/scripts/tweener.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import { Tween, Easing } from '@tweenjs/tween.js';
import { Material, Script, Vec2, Vec3, Vec4, Color } from 'playcanvas';

/** @enum {enum} */
const EasingTypes = {
In: 'In',
Out: 'Out',
InOut: 'InOut'
};

/** @enum {enum} */
const EasingFunctions = {
Linear: 'Linear',
Quadratic: 'Quadratic',
Cubic: 'Cubic',
Quartic: 'Quartic',
Quintic: 'Quintic',
Sinusoidal: 'Sinusoidal',
Exponential: 'Exponential',
Circular: 'Circular',
Elastic: 'Elastic',
Back: 'Back',
Bounce: 'Bounce'
};

/** @interface */
class TweenDescriptor { /* eslint-disable-line no-unused-vars */
/**
* Path to the property to tween
* @type {string}
* @attribute
*/
path;

/**
* Start value for the tween
* @type {Vec4}
* @attribute
*/
start;

/**
* End value for the tween
* @type {Vec4}
* @attribute
*/
end;

/**
* Duration of the tween in milliseconds
* @type {number}
* @attribute
*/
duration;

/**
* Delay before starting the tween in milliseconds
* @type {number}
* @attribute
*/
delay;

/**
* Number of times to repeat the tween
* @type {number}
* @attribute
*/
repeat;

/**
* Delay between repeats in milliseconds
* @type {number}
* @attribute
*/
repeatDelay;

/**
* Whether to reverse the tween on repeat
* @type {boolean}
* @attribute
*/
yoyo;

/**
* Index of the easing function to use
* @type {EasingFunctions}
* @attribute
*/
easingFunction = EasingFunctions.Linear;

/**
* Index of the easing type to use
* @type {EasingTypes}
* @attribute
*/
easingType = EasingTypes.InOut;

/**
* Event to fire when tween starts
* @type {string}
* @attribute
*/
startEvent;

/**
* Event to fire when tween stops
* @type {string}
* @attribute
*/
stopEvent;

/**
* Event to fire when tween updates
* @type {string}
* @attribute
*/
updateEvent;

/**
* Event to fire when tween completes
* @type {string}
* @attribute
*/
completeEvent;

/**
* Event to fire when tween repeats
* @type {string}
* @attribute
*/
repeatEvent;
}

export class Tweener extends Script {
/**
* Array of tween configurations
* @type {TweenDescriptor[]}
* @attribute
*/
tweens = [];

/**
* Array of active tween instances
* @type {Tween[]}
*/
tweenInstances = [];

getEasingFunction(tween) {
if (tween.easingFunction === 'Linear') {
return Easing.Linear.None;
}
return Easing[tween.easingFunction][tween.easingType];
}

createStartEndValues(property, start, end) {
if (typeof property === 'number') {
return [{ x: start.x }, { x: end.x }];
}

if (property instanceof Vec2) {
return [new Vec2(start.x, start.y), new Vec2(end.x, end.y)];
}

if (property instanceof Vec3) {
return [new Vec3(start.x, start.y, start.z), new Vec3(end.x, end.y, end.z)];
}

if (property instanceof Vec4) {
return [start.clone(), end.clone()];
}

if (property instanceof Color) {
return [
new Color(start.x, start.y, start.z, start.w),
new Color(end.x, end.y, end.z, end.w)
];
}

console.error('ERROR: tween - specified property must be a number, vec2, vec3, vec4 or color');
return [null, null];
}

handleSpecialProperties(propertyName, propertyOwner, value) {
switch (propertyName) {
case 'localPosition':
propertyOwner.setLocalPosition(value);
break;
case 'localEulerAngles':
propertyOwner.setLocalEulerAngles(value);
break;
case 'localScale':
propertyOwner.setLocalScale(value);
break;
case 'position':
propertyOwner.setPosition(value);
break;
case 'eulerAngles':
propertyOwner.setEulerAngles(value);
break;
}
return null;
}

play(idx) {
const tween = this.tweens[idx];
if (!tween) return;

// Stop any tweens that are animating the same property
this.tweenInstances.forEach((existingTween, i) => {
if (existingTween && this.tweens[i].path === tween.path) {
this.stop(i);
}
});

// Get property owner and name from path
const pathSegments = tween.path.split('.');
let propertyOwner = this.entity;
for (let i = 0; i < pathSegments.length - 1; i++) {
propertyOwner = propertyOwner[pathSegments[i]];
}

const propertyName = pathSegments[pathSegments.length - 1];
const property = propertyOwner[propertyName];
const isNumber = typeof property === 'number';

// Create start and end values
let [startValue, endValue] = this.createStartEndValues(property, tween.start, tween.end);
if (!startValue) return;

// Set initial value
propertyOwner[propertyName] = isNumber ? startValue.x : startValue;

// Handle special properties
const specialValues = this.handleSpecialProperties(propertyName, propertyOwner, startValue);
if (specialValues) {
[startValue, endValue] = specialValues;
}

// Update material if needed
if (propertyOwner instanceof Material) {
propertyOwner.update();
}

// Create and start the tween
this.tweenInstances[idx] = new Tween(startValue)
.to(endValue, tween.duration)
.easing(this.getEasingFunction(tween))
.onStart(() => {
if (tween.startEvent) {
this.app.fire(tween.startEvent);
}
})
.onStop(() => {
if (tween.stopEvent) {
this.app.fire(tween.stopEvent);
}
this.tweenInstances[idx] = null;
})
.onUpdate((obj) => {
propertyOwner[propertyName] = isNumber ? obj.x : obj;
this.handleSpecialProperties(propertyName, propertyOwner, obj);

if (propertyOwner instanceof Material) {
propertyOwner.update();
}

if (tween.updateEvent) {
this.app.fire(tween.updateEvent);
}
})
.onComplete(() => {
if (tween.completeEvent) {
this.app.fire(tween.completeEvent);
}
this.tweenInstances[idx] = null;
})
.onRepeat(() => {
if (tween.repeatEvent) {
this.app.fire(tween.repeatEvent);
}
})
.repeat(tween.repeat)
.repeatDelay(tween.repeatDelay)
.yoyo(tween.yoyo)
.delay(tween.delay)
.start();
}

stop(idx) {
this.tweenInstances[idx]?.stop();
this.tweenInstances[idx] = null;
}

update(dt) {
this.tweenInstances.forEach((tween) => {
tween?.update();
});
}
}
72 changes: 72 additions & 0 deletions examples/tween.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>PlayCanvas Web Components - Tweening</title>
<script type="importmap">
{
"imports": {
"playcanvas": "../node_modules/playcanvas/build/playcanvas.mjs",
"@tweenjs/tween.js": "../node_modules/@tweenjs/tween.js/dist/tween.esm.js"
}
}
</script>
<script type="module" src="../dist/pwc.mjs"></script>
<link rel="stylesheet" href="css/example.css">
</head>
<body>
<pc-app>
<pc-asset id="tweener" src="scripts/tweener.mjs" preload></pc-asset>
<pc-asset id="studio" src="assets/skies/octagon-lamps-photo-studio-2k.hdr" preload></pc-asset>
<pc-asset id="star" src="assets/models/star.glb" preload></pc-asset>
<!-- Scene -->
<pc-scene></pc-scene>
<!-- Sky -->
<pc-sky asset="studio" type="none" lighting></pc-sky>
<!-- Camera -->
<pc-entity name="camera" position="0 0 3">
<pc-camera clear-color="#8099e6"></pc-camera>
</pc-entity>
<!-- Star -->
<pc-entity name="star"
onpointerenter="this.entity.script.tweener.play(0)"
onpointerleave="this.entity.script.tweener.play(1)"
onpointerdown="this.entity.script.tweener.play(2)">
<pc-model asset="star"></pc-model>
<pc-scripts>
<pc-script name="tweener" attributes='{
"tweens": [
{
"path": "localScale",
"start": [1, 1, 1, 0],
"end": [1.2, 1.2, 1.2, 0],
"duration": 500,
"easingFunction": "Elastic",
"easingType": "Out"
},
{
"path": "localScale",
"start": [1.2, 1.2, 1.2, 0],
"end": [1, 1, 1, 0],
"duration": 500,
"easingFunction": "Elastic",
"easingType": "Out"
},
{
"path": "localEulerAngles",
"start": [0, 0, 0, 0],
"end": [0, 360, 0, 0],
"duration": 2000,
"easingFunction": "Elastic",
"easingType": "Out"
}
]
}'></pc-script>
</pc-scripts>
</pc-entity>
</pc-scene>
</pc-app>
<script type="module" src="scripts/ui.mjs"></script>
</body>
</html>
Loading
Loading