├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── tween.mjs └── tween.umd.js ├── eslint.config.mjs ├── index.d.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── easing.js ├── index.js ├── pc-entry.js ├── tween-manager.js └── tween.js └── test ├── tween.test.js └── utils ├── create-app.js └── jsdom.js /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@playcanvas.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | # HOW TO CONTRIBUTE 4 | 5 | 1. Read the coding standards below. 6 | 2. Keep PR simple and focused - one PR per feature. 7 | 3. Make a Pull Request. 8 | 4. Complete the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform). 9 | 5. Happy Days! :) 10 | 11 | #### Tips 12 | 13 | Feel free to contribute bug fixes or documentation fixes as pull request. 14 | 15 | If you are looking for ideas what to work on, head to [Issues](https://github.com/playcanvas/playcanvas-tween/issues) and checkout out open tickets or start a conversation. It is best to start conversation if you are going to make major changes to the engine or add significant features to get advice on how to approach it. [Forum](http://forum.playcanvas.com/) is good place to have a chat with community as well. 16 | 17 | Try to keep PR focused on a single feature, small PR's are easier to review and will get merged faster. Too large PR's are better be broken into smaller ones so they can be merged and tested on its own. 18 | 19 | # CODING STANDARDS 20 | 21 | ## General 22 | 23 | These coding standards are based on the [Google JavaScript Coding Standards](https://google.github.io/styleguide/javascriptguide.xml). If something is not defined here, use this guide as a backup. 24 | 25 | ### Keep it simple 26 | 27 | Simple code is always better. Modular (horizontal dependencies) code is easier to extend and work with, than with vertical dependencies. 28 | 29 | ### Use International/American English spelling 30 | 31 | For example, use "Initialize" instead of "Initialise", and "color" instead of "colour". 32 | 33 | ### Opening braces should be on the same line as the statement 34 | 35 | For example: 36 | ```javascript 37 | // Notice there is no new line before the opening brace 38 | function inc() { 39 | x++; 40 | } 41 | ``` 42 | 43 | Also use the following style for 'if' statements: 44 | ```javascript 45 | if (test) { 46 | // do something 47 | } else { 48 | // do something else 49 | } 50 | ``` 51 | 52 | If condition with body is small and is two-liner, can avoid using braces: 53 | ```javascript 54 | if (test === 0) 55 | then(); 56 | ``` 57 | 58 | ### Use spaces in preference to tabs 59 | 60 | Ensure that your IDE of choice is set up to insert '4 spaces' for every press of the Tab key and replaces tabs with spaces on save. Different browsers have different tab lengths and a mixture of tabs and spaces for indentation can create funky results. 61 | 62 | ### Remove all trailing spaces and ending line 63 | 64 | On save, set your text editor to remove trailing spaces and ensure there is an empty line at the end of the file. 65 | 66 | ### Use spaces between operators 67 | 68 | ```javascript 69 | var foo = 16 + 32 / 4; 70 | 71 | for (var i = 0, len = list.length; i < len; i++) { 72 | // ... 73 | } 74 | ``` 75 | 76 | ### Leave a space after the function keyword for anonymous functions 77 | ```javascript 78 | var fn = function () { 79 | 80 | }; 81 | ``` 82 | 83 | ### No spaces between () brackets 84 | ```javascript 85 | foo(); 86 | bar(1, 2); 87 | ``` 88 | 89 | ### Use spaces between [ ] and { } brackets 90 | ```javascript 91 | var a = { }; 92 | var b = { key: 'value' }; 93 | var c = [ ]; 94 | var d = [ 32, 64 ]; 95 | ``` 96 | 97 | ### No semicolon on closing function brace 98 | 99 | Semicolons are not needed to delimit the ends of functions. Follow the convention below: 100 | ```javascript 101 | function class() { 102 | } // Note the lack of semicolon here 103 | ``` 104 | 105 | Semicolons **are** needed if you're function is declared as a variable 106 | ```javascript 107 | var fn = function () { 108 | }; // Note the semicolon here 109 | ``` 110 | 111 | ### Put all variable declarations at the top of functions 112 | 113 | Variable declarations should all be placed first or close to the top of functions. This is because variables have a function-level scope. 114 | 115 | Variables should be declared one per line. 116 | 117 | ```javascript 118 | function fn() { 119 | var a = 0; 120 | var b = 1; 121 | var c = 2; 122 | } 123 | ``` 124 | ```javascript 125 | function fn() { 126 | var i; 127 | var bar = 0; 128 | 129 | for(i = 0; i < 32; ++i) { 130 | bar += i; 131 | } 132 | 133 | for(var i = 0; i < 32; i++) { } // don't do this, as i is already defined 134 | } 135 | ``` 136 | 137 | ### Exit logic early 138 | 139 | In functions exit early to simplify logic flow and avoid building indention-hell: 140 | ```javascript 141 | var foo = function (bar) { 142 | if (! bar) 143 | return; 144 | 145 | return bar + 32; 146 | }; 147 | ``` 148 | 149 | Same for iterators: 150 | ```javascript 151 | for(var i = 0; i < items.length; i++) { 152 | if (! items[i].test) 153 | continue; 154 | 155 | items[i].bar(); 156 | } 157 | ``` 158 | 159 | ## Naming 160 | 161 | ### Capitalization 162 | 163 | ```javascript 164 | // Namespace should have short lowercase names 165 | var namespace = { }; 166 | 167 | // Classes (or rather Constructors) should be CamelCase 168 | var MyClass = function () { }; 169 | 170 | // Variables should be mixedCase 171 | var mixedCase = 1; 172 | 173 | // Function are usually variables so should be mixedCase 174 | // (unless they are class constructors) 175 | var myFunction = function () { }; 176 | 177 | // Constants should be ALL_CAPITALS separated by underscores. 178 | // Note, ES5 doesn't support constants, 179 | // so this is just convention. 180 | var THIS_IS_CONSTANT = "well, kind of"; 181 | 182 | // Private variables should start with a leading underscore. 183 | // Note, you should attempt to make private variables actually private using 184 | // a closure. 185 | var _private = "private"; 186 | var _privateFn = function () { }; 187 | ``` 188 | 189 | ### Acronyms should not be upper-case, they should follow coding standards 190 | 191 | Treat acronyms like a normal word. e.g. 192 | ```javascript 193 | var json = ""; // not "JSON"; 194 | var id = 1; // not "ID"; 195 | 196 | function getId() { }; // not "getID" 197 | function loadJson() { }; // not "loadJSON" 198 | 199 | new HttpObject(); // not "HTTPObject"; 200 | ``` 201 | 202 | ### Use common callback names: 'success', 'error', (possibly 'callback') 203 | ```javascript 204 | function asyncFunction(success, error) { 205 | // do something 206 | } 207 | ``` 208 | ```javascript 209 | function asyncFunction(success) { 210 | // do something 211 | } 212 | ``` 213 | ```javascript 214 | function asyncFunction(callback) { 215 | // do something 216 | } 217 | ``` 218 | 219 | ### Cache the 'this' reference as 'self' 220 | 221 | It is often useful to be able to cache the 'this' object to get around the scoping behavior of JavaScript. If you need to do this, cache it in a variable called 'self'. 222 | 223 | ```javascript 224 | var self = this; 225 | ``` 226 | 227 | ### Avoid using function.bind(scope) 228 | 229 | ```javascript 230 | setTimeout(function() { 231 | this.foo(); 232 | }.bind(this)); // don't do this 233 | ``` 234 | 235 | Instead use `self` reference in upper scope: 236 | ```javascript 237 | var self = this; 238 | setTimeout(function() { 239 | self.foo(); 240 | }); 241 | ``` 242 | 243 | ## Privacy 244 | 245 | ### Make variables private if used only internally 246 | 247 | Variables that should be accessible only within class should start with `_`: 248 | ```javascript 249 | var Item = function () { 250 | this._a = "private"; 251 | }; 252 | Item.prototype.bar = function() { 253 | this._a += "!"; 254 | }; 255 | 256 | var foo = new Item(); 257 | foo._a += "?"; // not good 258 | ``` 259 | 260 | ## Object Member Iteration 261 | 262 | The hasOwnProperty() function should be used when iterating over an object's members. This is to avoid accidentally picking up unintended members that may have been added to the object's prototype. For example: 263 | 264 | ```javascript 265 | for (var key in values) { 266 | if (! values.hasOwnProperty(key)) 267 | continue; 268 | 269 | doStuff(values[key]); 270 | } 271 | ``` 272 | 273 | ## Source files 274 | 275 | ### Filenames should contain only class name 276 | 277 | Filenames should be all lower case with words separated by dashes. 278 | The usual format should be {{{file-name.js}}} 279 | 280 | e.g. 281 | ```javascript 282 | asset-registry.js 283 | graph-node.js 284 | ``` 285 | 286 | ## Namespaces and Classes 287 | 288 | Use library function pc.extend to add additional Classes, methods and variables on to an existing namespace. 289 | Private functions and variables should be declared inside the namespace. 290 | Avoid declaring multiple Classes and Methods extending namespace per file. 291 | 292 | ```javascript 293 | pc.extend(pc, function() { 294 | var Class = function () { 295 | 296 | }; 297 | 298 | // optionally can inherit 299 | Class = pc.inherits(Class, pc.Base); 300 | 301 | Class.prototype.derivedFn = function () { 302 | 303 | }; 304 | 305 | return { 306 | Class: Class 307 | }; 308 | }()); 309 | ``` 310 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This issue tracker is **only** for bug reports and feature requests. For general support/help, visit https://forum.playcanvas.com 2 | 3 | For bug reports, include: 4 | 5 | ### Description 6 | 7 | Provide as much information as possible. Include (where applicable): 8 | 9 | * URL to a simple, reproducible test case that illustrates the problem clearly 10 | * Screenshots 11 | * The platform(s)/browser(s) where the issue is seen 12 | 13 | ### Steps to Reproduce 14 | 15 | 1. 16 | 2. 17 | 3. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | I confirm I have signed the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform). 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | paths-ignore: ['README.md', 'LICENSE'] 8 | pull_request: 9 | branches: [ main ] 10 | paths-ignore: ['README.md', 'LICENSE'] 11 | 12 | concurrency: 13 | group: ci-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | runs-on: ubuntu-latest 20 | 21 | timeout-minutes: 10 22 | 23 | strategy: 24 | matrix: 25 | node-version: [18.x] 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v3 30 | 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | cache: 'npm' 36 | 37 | - name: Install 38 | run: npm ci 39 | 40 | - name: Lint 41 | run: npm run lint 42 | 43 | test: 44 | name: Test 45 | runs-on: ubuntu-latest 46 | 47 | timeout-minutes: 10 48 | 49 | strategy: 50 | matrix: 51 | node-version: [18.x] 52 | 53 | steps: 54 | - name: Checkout code 55 | uses: actions/checkout@v3 56 | 57 | - name: Use Node.js ${{ matrix.node-version }} 58 | uses: actions/setup-node@v3 59 | with: 60 | node-version: ${{ matrix.node-version }} 61 | cache: 'npm' 62 | 63 | - name: Install 64 | run: npm ci 65 | 66 | - name: Test 67 | run: npm run test 68 | 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .idea/ 3 | .vscode/ 4 | node_modules 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2021 PlayCanvas Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a tween library for PlayCanvas. You can include `tween.js` in your Editor Project to start using the library. 4 | 5 | If you create your application *after* loading this library, you can call the following method to enable 6 | tweening on your application: 7 | 8 | ```javascript 9 | app.addTweenManager(); 10 | ``` 11 | 12 | # Usage 13 | 14 | ## Editor 15 | 16 | Copy the the [tween.umd.js](./dist/tween.umd.js) file from the build directory into your editor project 17 | 18 | Tweening `pc.Entity` properties looks like this: 19 | 20 | ```javascript 21 | var tween = entity.tween(fromProperty).to(toProperty, duration, easing); 22 | tween.start(); 23 | ``` 24 | 25 | For example, this tweens the entity's local position with duration 1.0 and `SineOut` easing: 26 | 27 | ```javascript 28 | var tween = entity.tween(entity.getLocalPosition()).to({x: 10, y: 0, z: 0}, 1.0, pc.SineOut); 29 | tween.start(); 30 | ``` 31 | 32 | If you are dealing with rotations you should use `rotate` instead of `to`. This takes euler angles and uses an internal quaternion or it can take quaternions to slerp between angles. For example: 33 | 34 | ```javascript 35 | entity.tween(entity.getLocalEulerAngles()).rotate({x: 0, y: 180, z: 0}, 1.0, pc.Linear); 36 | ``` 37 | 38 | ```javascript 39 | entity.tween(entity.getLocalRotation()).rotate(targetQuaternionRotation, 1.0, pc.Linear); 40 | ``` 41 | 42 | You can also tween properties of any other object not just entities. For example: 43 | 44 | ```javascript 45 | // some object with a property called 'value' 46 | var data = { 47 | value: 0 48 | }; 49 | 50 | // create a new tween using pc.Application#tween 51 | // passing another object as the target. Any properties 52 | // that are common between the target and the source object 53 | // will be tweened 54 | app.tween(data).to({value: 1}, 1.0, pc.BackOut); 55 | ``` 56 | 57 | ## ESM 58 | 59 | You can also use this library as an ES Module, either from withing an ESM Script or an engine only project 60 | 61 | Save the esm [`tween.mjs`](./dist/tween.mjs) to your project and import it. 62 | 63 | ```javascript 64 | import { tweenEntity, SineOut } from './tween.mjs' 65 | 66 | tweenEntity(entity, entity.getLocalPosition()) 67 | .to({x: 10}, 1.0, SineOut)); 68 | ``` 69 | 70 | The ESM won't automatically add `tween()` methods to Entity and Application, however you can manually add these using `addTweenExtensions(pc)` 71 | 72 | ```javascript 73 | import { addTweenExtensions, SineOut } from './tween.mjs' 74 | import * as pc from 'playcanvas' 75 | 76 | // Adds .tween() to Entity and Application 77 | addTweenExtensions(pc); 78 | 79 | entity.tween(entity.getLocalPosition()) 80 | .to({x: 10}, 1.0, SineOut)); 81 | ``` 82 | 83 | # Chaining 84 | 85 | You can chain method calls for a tween. For example: 86 | 87 | ```javascript 88 | // delay, yoyo and loop tween 89 | entity 90 | .tween(entity.getLocalPosition()).to({x: 10, y: 0, z: 0}, 1.0, pc.SineOut) 91 | .delay(1.0) 92 | .yoyo(true) 93 | .loop(true) 94 | .start(); 95 | ``` 96 | 97 | # Methods 98 | 99 | ## `start()` 100 | 101 | To start playing a tween call `tween.start()`. 102 | 103 | ## `stop()` 104 | 105 | To stop a tween call `tween.stop()`. 106 | 107 | ## `pause()` 108 | 109 | To pause a tween call `tween.pause()`. 110 | 111 | ## `resume()` 112 | 113 | To resume a paused tween call `tween.resume()`. 114 | 115 | ## `delay(duration)` 116 | 117 | To delay a tween call `tween.delay(duration)` where duration is in seconds. 118 | 119 | ## `repeat(count)` 120 | 121 | To repeat a tween `count` times call `tween.repeat(count)`. 122 | 123 | ## `loop(true / false)` 124 | 125 | To loop a tween forever call `tween.loop(true)`. 126 | 127 | ## `yoyo(true / false)` 128 | 129 | To make a tween play in reverse after it finishes call `tween.yoyo(true)`. Note that to actually see the tween play in reverse in the end, you have to either repeat the tween at least 2 times or set it to loop forever. E.g. to only play a tween from start to end and then from end to start 1 time you need to do: 130 | ```javascript 131 | tween.yoyo(true).repeat(2); 132 | ``` 133 | 134 | ## `reverse()` 135 | 136 | To reverse a tween call `tween.reverse()`. 137 | 138 | # Events 139 | 140 | To subscribe to events during Tween execution, use a these methods: 141 | 142 | ## `onUpdate` 143 | 144 | This is called on every update cycle. You can use this method to manually update something in your code using the tweened value. 145 | It provides `dt` argument. 146 | 147 | E.g. 148 | 149 | ```javascript 150 | var color = new pc.Color(1, 0, 0); 151 | 152 | var tween = app.tween(color).to(new pc.Color(0, 1, 1), 1, pc.Linear); 153 | tween.onUpdate((dt) => { 154 | material.diffuse = color; 155 | material.update(); 156 | }); 157 | ``` 158 | 159 | ## `onComplete` 160 | 161 | This is called when the tween is finished. If the tween is looping the `onLoop` will be called instead. 162 | 163 | E.g. 164 | 165 | ```javascript 166 | entity 167 | .tween(entity.getLocalPosition()) 168 | .to({x: 10, y: 0, z: 0}, 1, pc.Linear) 169 | .onComplete(() => { 170 | console.log('tween completed'); 171 | }); 172 | ``` 173 | 174 | ## `onLoop` 175 | 176 | This is called whenever a looping tween finishes a cycle. This is called instead of the `onComplete` for looping tweens. 177 | 178 | E.g. 179 | 180 | ```javascript 181 | entity 182 | .tween(entity.getLocalPosition()) 183 | .to({x: 10, y: 0, z: 0}, 1, pc.Linear) 184 | .loop(true) 185 | .onLoop(() => { 186 | console.log('tween loop'); 187 | }); 188 | ``` 189 | 190 | # Easing methods 191 | 192 | There are various easing methods you can use that change the way that values are interpolated. The available easing methods are: 193 | 194 | - `pc.Linear` 195 | - `pc.QuadraticIn` 196 | - `pc.QuadraticOut` 197 | - `pc.QuadraticInOut` 198 | - `pc.CubicIn` 199 | - `pc.CubicOut` 200 | - `pc.CubicInOut` 201 | - `pc.QuarticIn` 202 | - `pc.QuarticOut` 203 | - `pc.QuarticInOut` 204 | - `pc.QuinticIn` 205 | - `pc.QuinticOut` 206 | - `pc.QuinticInOut` 207 | - `pc.SineIn` 208 | - `pc.SineOut` 209 | - `pc.SineInOut` 210 | - `pc.ExponentialIn` 211 | - `pc.ExponentialOut` 212 | - `pc.ExponentialInOut` 213 | - `pc.CircularIn` 214 | - `pc.CircularOut` 215 | - `pc.CircularInOut` 216 | - `pc.BackIn` 217 | - `pc.BackOut` 218 | - `pc.BackInOut` 219 | - `pc.BounceIn` 220 | - `pc.BounceOut` 221 | - `pc.BounceInOut` 222 | - `pc.ElasticIn` 223 | - `pc.ElasticOut` 224 | - `pc.ElasticInOut` 225 | 226 | # Tutorial 227 | 228 | You can find a tutorial with various use cases [here][1]. 229 | 230 | [1]: http://developer.playcanvas.com/en/tutorials/tweening/ 231 | -------------------------------------------------------------------------------- /dist/tween.mjs: -------------------------------------------------------------------------------- 1 | import { EventHandler, Quat, Vec2, Vec3, Vec4, Color, AppBase } from 'playcanvas'; 2 | 3 | /** @import { Tween } from "./tween.js" */ 4 | 5 | /** 6 | * @name TweenManager 7 | * @description Handles updating tweens 8 | */ 9 | class TweenManager { 10 | /** 11 | * @private 12 | * @type {Tween[]} 13 | */ 14 | _tweens = []; 15 | 16 | /** 17 | * @private 18 | * @type {Tween[]} 19 | */ 20 | _add = []; 21 | 22 | /** 23 | * Adds a tween 24 | * @param {Tween} tween - The tween instance to manage 25 | * @returns {Tween} - The tween instance for chaining 26 | */ 27 | add(tween) { 28 | this._add.push(tween); 29 | return tween; 30 | } 31 | 32 | /** 33 | * Update the tween 34 | * @param {number} dt - The delta time 35 | */ 36 | update(dt) { 37 | let i = 0; 38 | let n = this._tweens.length; 39 | while (i < n) { 40 | if (this._tweens[i].update(dt)) { 41 | i++; 42 | } else { 43 | this._tweens.splice(i, 1); 44 | n--; 45 | } 46 | } 47 | 48 | // add any tweens that were added mid-update 49 | if (this._add.length) { 50 | for (let i = 0; i < this._add.length; i++) { 51 | if (this._tweens.indexOf(this._add[i]) > -1) continue; 52 | this._tweens.push(this._add[i]); 53 | } 54 | this._add.length = 0; 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Easing methods 61 | */ 62 | 63 | const Linear = function (k) { 64 | return k; 65 | }; 66 | 67 | const QuadraticIn = function (k) { 68 | return k * k; 69 | }; 70 | 71 | const QuadraticOut = function (k) { 72 | return k * (2 - k); 73 | }; 74 | 75 | const QuadraticInOut = function (k) { 76 | if ((k *= 2) < 1) { 77 | return 0.5 * k * k; 78 | } 79 | return -0.5 * (--k * (k - 2) - 1); 80 | }; 81 | 82 | const CubicIn = function (k) { 83 | return k * k * k; 84 | }; 85 | 86 | const CubicOut = function (k) { 87 | return --k * k * k + 1; 88 | }; 89 | 90 | const CubicInOut = function (k) { 91 | if ((k *= 2) < 1) return 0.5 * k * k * k; 92 | return 0.5 * ((k -= 2) * k * k + 2); 93 | }; 94 | 95 | const QuarticIn = function (k) { 96 | return k * k * k * k; 97 | }; 98 | 99 | const QuarticOut = function (k) { 100 | return 1 - (--k * k * k * k); 101 | }; 102 | 103 | const QuarticInOut = function (k) { 104 | if ((k *= 2) < 1) return 0.5 * k * k * k * k; 105 | return -0.5 * ((k -= 2) * k * k * k - 2); 106 | }; 107 | 108 | const QuinticIn = function (k) { 109 | return k * k * k * k * k; 110 | }; 111 | 112 | const QuinticOut = function (k) { 113 | return --k * k * k * k * k + 1; 114 | }; 115 | 116 | const QuinticInOut = function (k) { 117 | if ((k *= 2) < 1) return 0.5 * k * k * k * k * k; 118 | return 0.5 * ((k -= 2) * k * k * k * k + 2); 119 | }; 120 | 121 | const SineIn = function (k) { 122 | if (k === 0) return 0; 123 | if (k === 1) return 1; 124 | return 1 - Math.cos(k * Math.PI / 2); 125 | }; 126 | 127 | const SineOut = function (k) { 128 | if (k === 0) return 0; 129 | if (k === 1) return 1; 130 | return Math.sin(k * Math.PI / 2); 131 | }; 132 | 133 | const SineInOut = function (k) { 134 | if (k === 0) return 0; 135 | if (k === 1) return 1; 136 | return 0.5 * (1 - Math.cos(Math.PI * k)); 137 | }; 138 | 139 | const ExponentialIn = function (k) { 140 | return k === 0 ? 0 : Math.pow(1024, k - 1); 141 | }; 142 | 143 | const ExponentialOut = function (k) { 144 | return k === 1 ? 1 : 1 - Math.pow(2, -10 * k); 145 | }; 146 | 147 | const ExponentialInOut = function (k) { 148 | if (k === 0) return 0; 149 | if (k === 1) return 1; 150 | if ((k *= 2) < 1) return 0.5 * Math.pow(1024, k - 1); 151 | return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2); 152 | }; 153 | 154 | const CircularIn = function (k) { 155 | return 1 - Math.sqrt(1 - k * k); 156 | }; 157 | 158 | const CircularOut = function (k) { 159 | return Math.sqrt(1 - (--k * k)); 160 | }; 161 | 162 | const CircularInOut = function (k) { 163 | if ((k *= 2) < 1) return -0.5 * (Math.sqrt(1 - k * k) - 1); 164 | return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); 165 | }; 166 | 167 | const ElasticIn = function (k) { 168 | const p = 0.4; 169 | let s, a = 0.1; 170 | if (k === 0) return 0; 171 | if (k === 1) return 1; 172 | if (!a || a < 1) { 173 | a = 1; s = p / 4; 174 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 175 | return -(a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 176 | }; 177 | 178 | const ElasticOut = function (k) { 179 | const p = 0.4; 180 | let s, a = 0.1; 181 | if (k === 0) return 0; 182 | if (k === 1) return 1; 183 | if (!a || a < 1) { 184 | a = 1; s = p / 4; 185 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 186 | return (a * Math.pow(2, -10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1); 187 | }; 188 | 189 | const ElasticInOut = function (k) { 190 | const p = 0.4; 191 | let s, a = 0.1; 192 | if (k === 0) return 0; 193 | if (k === 1) return 1; 194 | if (!a || a < 1) { 195 | a = 1; s = p / 4; 196 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 197 | if ((k *= 2) < 1) return -0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 198 | return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; 199 | }; 200 | 201 | const BackIn = function (k) { 202 | const s = 1.70158; 203 | return k * k * ((s + 1) * k - s); 204 | }; 205 | 206 | const BackOut = function (k) { 207 | const s = 1.70158; 208 | return --k * k * ((s + 1) * k + s) + 1; 209 | }; 210 | 211 | const BackInOut = function (k) { 212 | const s = 1.70158 * 1.525; 213 | if ((k *= 2) < 1) return 0.5 * (k * k * ((s + 1) * k - s)); 214 | return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); 215 | }; 216 | 217 | const BounceOut = function (k) { 218 | if (k < (1 / 2.75)) { 219 | return 7.5625 * k * k; 220 | } else if (k < (2 / 2.75)) { 221 | return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; 222 | } else if (k < (2.5 / 2.75)) { 223 | return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; 224 | } 225 | return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; 226 | 227 | }; 228 | 229 | const BounceIn = function (k) { 230 | return 1 - BounceOut(1 - k); 231 | }; 232 | 233 | const BounceInOut = function (k) { 234 | if (k < 0.5) return BounceIn(k * 2) * 0.5; 235 | return BounceOut(k * 2 - 1) * 0.5 + 0.5; 236 | }; 237 | 238 | /** @import { TweenManager } from "./tween-manager.js" */ 239 | /** @import { Entity } from "playcanvas" */ 240 | 241 | class Tween extends EventHandler { 242 | /** 243 | * @name Tween 244 | * @param {object} target - The target property that will be tweened 245 | * @param {TweenManager} manager - The tween manager 246 | * @param {Entity} entity - The Entity whose property we are tween-ing 247 | */ 248 | constructor(target, manager, entity) { 249 | 250 | super(); 251 | 252 | this.manager = manager; 253 | 254 | if (entity) { 255 | this.entity = null; // if present the tween will dirty the transforms after modify the target 256 | } 257 | 258 | this.time = 0; 259 | 260 | this.complete = false; 261 | this.playing = false; 262 | this.stopped = true; 263 | this.pending = false; 264 | 265 | this.target = target; 266 | 267 | this.duration = 0; 268 | this._currentDelay = 0; 269 | this.timeScale = 1; 270 | this._reverse = false; 271 | 272 | this._delay = 0; 273 | this._yoyo = false; 274 | 275 | this._count = 0; 276 | this._numRepeats = 0; 277 | this._repeatDelay = 0; 278 | 279 | this._from = false; // indicates a "from" tween 280 | 281 | // for rotation tween 282 | this._slerp = false; // indicates a rotation tween 283 | this._fromQuat = new Quat(); 284 | this._toQuat = new Quat(); 285 | this._quat = new Quat(); 286 | 287 | this.easing = Linear; 288 | 289 | this._sv = {}; // start values 290 | this._ev = {}; // end values 291 | } 292 | 293 | _parseProperties(properties) { 294 | let _properties; 295 | if (properties instanceof Vec2) { 296 | _properties = { 297 | x: properties.x, 298 | y: properties.y 299 | }; 300 | } else if (properties instanceof Vec3) { 301 | _properties = { 302 | x: properties.x, 303 | y: properties.y, 304 | z: properties.z 305 | }; 306 | } else if (properties instanceof Vec4) { 307 | _properties = { 308 | x: properties.x, 309 | y: properties.y, 310 | z: properties.z, 311 | w: properties.w 312 | }; 313 | } else if (properties instanceof Quat) { 314 | _properties = { 315 | x: properties.x, 316 | y: properties.y, 317 | z: properties.z, 318 | w: properties.w 319 | }; 320 | } else if (properties instanceof Color) { 321 | _properties = { 322 | r: properties.r, 323 | g: properties.g, 324 | b: properties.b 325 | }; 326 | if (properties.a !== undefined) { 327 | _properties.a = properties.a; 328 | } 329 | } else { 330 | _properties = properties; 331 | } 332 | return _properties; 333 | } 334 | 335 | 336 | // properties - js obj of values to update in target 337 | to(properties, duration, easing, delay, repeat, yoyo) { 338 | this._properties = this._parseProperties(properties); 339 | this.duration = duration; 340 | 341 | if (easing) this.easing = easing; 342 | if (delay) { 343 | this.delay(delay); 344 | } 345 | if (repeat) { 346 | this.repeat(repeat); 347 | } 348 | 349 | if (yoyo) { 350 | this.yoyo(yoyo); 351 | } 352 | 353 | return this; 354 | } 355 | 356 | from(properties, duration, easing, delay, repeat, yoyo) { 357 | this._properties = this._parseProperties(properties); 358 | this.duration = duration; 359 | 360 | if (easing) this.easing = easing; 361 | if (delay) { 362 | this.delay(delay); 363 | } 364 | if (repeat) { 365 | this.repeat(repeat); 366 | } 367 | 368 | if (yoyo) { 369 | this.yoyo(yoyo); 370 | } 371 | 372 | this._from = true; 373 | 374 | return this; 375 | } 376 | 377 | rotate(properties, duration, easing, delay, repeat, yoyo) { 378 | this._properties = this._parseProperties(properties); 379 | 380 | this.duration = duration; 381 | 382 | if (easing) this.easing = easing; 383 | if (delay) { 384 | this.delay(delay); 385 | } 386 | if (repeat) { 387 | this.repeat(repeat); 388 | } 389 | 390 | if (yoyo) { 391 | this.yoyo(yoyo); 392 | } 393 | 394 | this._slerp = true; 395 | 396 | return this; 397 | } 398 | 399 | start() { 400 | let prop, _x, _y, _z; 401 | 402 | this.playing = true; 403 | this.complete = false; 404 | this.stopped = false; 405 | this._count = 0; 406 | this.pending = (this._delay > 0); 407 | 408 | if (this._reverse && !this.pending) { 409 | this.time = this.duration; 410 | } else { 411 | this.time = 0; 412 | } 413 | 414 | if (this._from) { 415 | for (prop in this._properties) { 416 | if (this._properties.hasOwnProperty(prop)) { 417 | this._sv[prop] = this._properties[prop]; 418 | this._ev[prop] = this.target[prop]; 419 | } 420 | } 421 | 422 | if (this._slerp) { 423 | this._toQuat.setFromEulerAngles(this.target.x, this.target.y, this.target.z); 424 | 425 | _x = this._properties.x !== undefined ? this._properties.x : this.target.x; 426 | _y = this._properties.y !== undefined ? this._properties.y : this.target.y; 427 | _z = this._properties.z !== undefined ? this._properties.z : this.target.z; 428 | this._fromQuat.setFromEulerAngles(_x, _y, _z); 429 | } 430 | } else { 431 | for (prop in this._properties) { 432 | if (this._properties.hasOwnProperty(prop)) { 433 | this._sv[prop] = this.target[prop]; 434 | this._ev[prop] = this._properties[prop]; 435 | } 436 | } 437 | 438 | if (this._slerp) { 439 | _x = this._properties.x !== undefined ? this._properties.x : this.target.x; 440 | _y = this._properties.y !== undefined ? this._properties.y : this.target.y; 441 | _z = this._properties.z !== undefined ? this._properties.z : this.target.z; 442 | 443 | if (this._properties.w !== undefined) { 444 | this._fromQuat.copy(this.target); 445 | this._toQuat.set(_x, _y, _z, this._properties.w); 446 | } else { 447 | this._fromQuat.setFromEulerAngles(this.target.x, this.target.y, this.target.z); 448 | this._toQuat.setFromEulerAngles(_x, _y, _z); 449 | } 450 | } 451 | } 452 | 453 | // set delay 454 | this._currentDelay = this._delay; 455 | 456 | // add to manager when started 457 | this.manager.add(this); 458 | 459 | return this; 460 | } 461 | 462 | pause() { 463 | this.playing = false; 464 | } 465 | 466 | resume() { 467 | this.playing = true; 468 | } 469 | 470 | stop() { 471 | this.playing = false; 472 | this.stopped = true; 473 | } 474 | 475 | delay(delay) { 476 | this._delay = delay; 477 | this.pending = true; 478 | 479 | return this; 480 | } 481 | 482 | repeat(num, delay) { 483 | this._count = 0; 484 | this._numRepeats = num; 485 | if (delay) { 486 | this._repeatDelay = delay; 487 | } else { 488 | this._repeatDelay = 0; 489 | } 490 | 491 | return this; 492 | } 493 | 494 | loop(loop) { 495 | if (loop) { 496 | this._count = 0; 497 | this._numRepeats = Infinity; 498 | } else { 499 | this._numRepeats = 0; 500 | } 501 | 502 | return this; 503 | } 504 | 505 | yoyo(yoyo) { 506 | this._yoyo = yoyo; 507 | return this; 508 | } 509 | 510 | reverse() { 511 | this._reverse = !this._reverse; 512 | 513 | return this; 514 | } 515 | 516 | chain() { 517 | let n = arguments.length; 518 | 519 | while (n--) { 520 | if (n > 0) { 521 | arguments[n - 1]._chained = arguments[n]; 522 | } else { 523 | this._chained = arguments[n]; 524 | } 525 | } 526 | 527 | return this; 528 | } 529 | 530 | onUpdate(callback) { 531 | this.on('update', callback); 532 | return this; 533 | } 534 | 535 | onComplete(callback) { 536 | this.on('complete', callback); 537 | return this; 538 | } 539 | 540 | onLoop(callback) { 541 | this.on('loop', callback); 542 | return this; 543 | } 544 | 545 | update(dt) { 546 | if (this.stopped) return false; 547 | 548 | if (!this.playing) return true; 549 | 550 | if (!this._reverse || this.pending) { 551 | this.time += dt * this.timeScale; 552 | } else { 553 | this.time -= dt * this.timeScale; 554 | } 555 | 556 | // delay start if required 557 | if (this.pending) { 558 | if (this.time > this._currentDelay) { 559 | if (this._reverse) { 560 | this.time = this.duration - (this.time - this._currentDelay); 561 | } else { 562 | this.time -= this._currentDelay; 563 | } 564 | this.pending = false; 565 | } else { 566 | return true; 567 | } 568 | } 569 | 570 | let _extra = 0; 571 | if ((!this._reverse && this.time > this.duration) || (this._reverse && this.time < 0)) { 572 | this._count++; 573 | this.complete = true; 574 | this.playing = false; 575 | if (this._reverse) { 576 | _extra = this.duration - this.time; 577 | this.time = 0; 578 | } else { 579 | _extra = this.time - this.duration; 580 | this.time = this.duration; 581 | } 582 | } 583 | 584 | const elapsed = (this.duration === 0) ? 1 : (this.time / this.duration); 585 | 586 | // run easing 587 | const a = this.easing(elapsed); 588 | 589 | // increment property 590 | let s, e; 591 | for (const prop in this._properties) { 592 | if (this._properties.hasOwnProperty(prop)) { 593 | s = this._sv[prop]; 594 | e = this._ev[prop]; 595 | this.target[prop] = s + (e - s) * a; 596 | } 597 | } 598 | 599 | if (this._slerp) { 600 | this._quat.slerp(this._fromQuat, this._toQuat, a); 601 | } 602 | 603 | // if this is a entity property then we should dirty the transform 604 | if (this.entity) { 605 | this.entity._dirtifyLocal(); 606 | 607 | // apply element property changes 608 | if (this.element && this.entity.element) { 609 | this.entity.element[this.element] = this.target; 610 | } 611 | 612 | if (this._slerp) { 613 | this.entity.setLocalRotation(this._quat); 614 | } 615 | } 616 | 617 | this.fire('update', dt); 618 | 619 | if (this.complete) { 620 | const repeat = this._repeat(_extra); 621 | if (!repeat) { 622 | this.fire('complete', _extra); 623 | if (this.entity) { 624 | this.entity.off('destroy', this.stop, this); 625 | } 626 | if (this._chained) this._chained.start(); 627 | } else { 628 | this.fire('loop'); 629 | } 630 | 631 | return repeat; 632 | } 633 | 634 | return true; 635 | } 636 | 637 | _repeat(extra) { 638 | // test for repeat conditions 639 | if (this._count < this._numRepeats) { 640 | // do a repeat 641 | if (this._reverse) { 642 | this.time = this.duration - extra; 643 | } else { 644 | this.time = extra; // include overspill time 645 | } 646 | this.complete = false; 647 | this.playing = true; 648 | 649 | this._currentDelay = this._repeatDelay; 650 | this.pending = true; 651 | 652 | if (this._yoyo) { 653 | // swap start/end properties 654 | for (const prop in this._properties) { 655 | const tmp = this._sv[prop]; 656 | this._sv[prop] = this._ev[prop]; 657 | this._ev[prop] = tmp; 658 | } 659 | 660 | if (this._slerp) { 661 | this._quat.copy(this._fromQuat); 662 | this._fromQuat.copy(this._toQuat); 663 | this._toQuat.copy(this._quat); 664 | } 665 | } 666 | 667 | return true; 668 | } 669 | return false; 670 | } 671 | } 672 | 673 | const managers = new Map(); 674 | 675 | /** 676 | * Registers a tween manager with a playcanvas application, so that it will update with the 677 | * applications frame update. 678 | * 679 | * @param {AppBase} app - The playcanvas application to register with the Tween Manager 680 | * @returns {TweenManager} - The registered TweenManager 681 | */ 682 | const getTweenManager = (app) => { 683 | 684 | if (!app || !(app instanceof AppBase)) { 685 | throw new Error('`getTweenManager` expects an instance of `AppBase`'); 686 | } 687 | 688 | if (!managers.has(app)) { 689 | const tweenManager = new TweenManager(); 690 | managers.set(app, tweenManager); 691 | app.on('update', (dt) => { 692 | tweenManager.update(dt); 693 | }); 694 | app.on('destroy', () => managers.delete(app)); 695 | } 696 | 697 | return managers.get(app); 698 | }; 699 | 700 | /** 701 | * Tweens an entities properties. 702 | * 703 | * @param {Entity} entity - The entity target to tween 704 | * @param {object} target - An object representing the properties to tween 705 | * @param {object} options - The tween options 706 | * @returns {Tween} - The tween instance 707 | * 708 | * @example 709 | * ``` 710 | * tweenEntity(entity, entity.getLocalPosition) 711 | * .to({x: 10, y: 0, z: 0}, 1, SineOut); 712 | * ``` 713 | */ 714 | const tweenEntity = (entity, target, options) => { 715 | 716 | const tweenManager = getTweenManager(entity._app); 717 | const tween = new Tween(target, tweenManager); 718 | tween.entity = entity; 719 | 720 | entity.once('destroy', tween.stop, tween); 721 | 722 | if (options && options.element) { 723 | // specify a element property to be updated 724 | tween.element = options.element; 725 | } 726 | 727 | return tween; 728 | }; 729 | 730 | /** 731 | * This function extends the `Entity` and `AppBase` class of PlayCanvas 732 | * with convenience methods such as `app.tween()` and `entity.tween()`. 733 | * 734 | * @param {{ AppBase, Entity }} pc - the playcanvas engine 735 | * 736 | * @example 737 | * ``` 738 | * import * as pc from 'playcanvas' 739 | * addTweenExtensions(pc) 740 | * entity.tween(); // new utility method 741 | * app.tween(pc.Color(1, 0, 0)) 742 | * ``` 743 | */ 744 | const addTweenExtensions = ({ AppBase, Entity }) => { 745 | 746 | if (!AppBase) { 747 | throw new Error('The param `addExtensions` must contain the `AppBase` class. `addExtensions({ AppBase })`'); 748 | } 749 | 750 | if (!Entity) { 751 | throw new Error('The param `addExtensions` must contain the `Entity` class. `addExtensions({ Entity })`'); 752 | } 753 | 754 | // Add pc.AppBase#tween method 755 | AppBase.prototype.tween = function (target) { 756 | const tweenManager = getTweenManager(this); 757 | return new Tween(target, tweenManager); 758 | }; 759 | 760 | // Add pc.Entity#tween method 761 | Entity.prototype.tween = function (target, options) { 762 | return tweenEntity(this, target, options); 763 | }; 764 | }; 765 | 766 | export { BackIn, BackInOut, BackOut, BounceIn, BounceInOut, BounceOut, CircularIn, CircularInOut, CircularOut, CubicIn, CubicInOut, CubicOut, ElasticIn, ElasticInOut, ElasticOut, ExponentialIn, ExponentialInOut, ExponentialOut, Linear, QuadraticIn, QuadraticInOut, QuadraticOut, QuarticIn, QuarticInOut, QuarticOut, QuinticIn, QuinticInOut, QuinticOut, SineIn, SineInOut, SineOut, addTweenExtensions, getTweenManager, tweenEntity }; 767 | -------------------------------------------------------------------------------- /dist/tween.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('playcanvas')) : 3 | typeof define === 'function' && define.amd ? define(['playcanvas'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.pc)); 5 | })(this, (function (playcanvas) { 'use strict'; 6 | 7 | /** 8 | * Easing methods 9 | */ 10 | 11 | const Linear = function (k) { 12 | return k; 13 | }; 14 | 15 | const QuadraticIn = function (k) { 16 | return k * k; 17 | }; 18 | 19 | const QuadraticOut = function (k) { 20 | return k * (2 - k); 21 | }; 22 | 23 | const QuadraticInOut = function (k) { 24 | if ((k *= 2) < 1) { 25 | return 0.5 * k * k; 26 | } 27 | return -0.5 * (--k * (k - 2) - 1); 28 | }; 29 | 30 | const CubicIn = function (k) { 31 | return k * k * k; 32 | }; 33 | 34 | const CubicOut = function (k) { 35 | return --k * k * k + 1; 36 | }; 37 | 38 | const CubicInOut = function (k) { 39 | if ((k *= 2) < 1) return 0.5 * k * k * k; 40 | return 0.5 * ((k -= 2) * k * k + 2); 41 | }; 42 | 43 | const QuarticIn = function (k) { 44 | return k * k * k * k; 45 | }; 46 | 47 | const QuarticOut = function (k) { 48 | return 1 - (--k * k * k * k); 49 | }; 50 | 51 | const QuarticInOut = function (k) { 52 | if ((k *= 2) < 1) return 0.5 * k * k * k * k; 53 | return -0.5 * ((k -= 2) * k * k * k - 2); 54 | }; 55 | 56 | const QuinticIn = function (k) { 57 | return k * k * k * k * k; 58 | }; 59 | 60 | const QuinticOut = function (k) { 61 | return --k * k * k * k * k + 1; 62 | }; 63 | 64 | const QuinticInOut = function (k) { 65 | if ((k *= 2) < 1) return 0.5 * k * k * k * k * k; 66 | return 0.5 * ((k -= 2) * k * k * k * k + 2); 67 | }; 68 | 69 | const SineIn = function (k) { 70 | if (k === 0) return 0; 71 | if (k === 1) return 1; 72 | return 1 - Math.cos(k * Math.PI / 2); 73 | }; 74 | 75 | const SineOut = function (k) { 76 | if (k === 0) return 0; 77 | if (k === 1) return 1; 78 | return Math.sin(k * Math.PI / 2); 79 | }; 80 | 81 | const SineInOut = function (k) { 82 | if (k === 0) return 0; 83 | if (k === 1) return 1; 84 | return 0.5 * (1 - Math.cos(Math.PI * k)); 85 | }; 86 | 87 | const ExponentialIn = function (k) { 88 | return k === 0 ? 0 : Math.pow(1024, k - 1); 89 | }; 90 | 91 | const ExponentialOut = function (k) { 92 | return k === 1 ? 1 : 1 - Math.pow(2, -10 * k); 93 | }; 94 | 95 | const ExponentialInOut = function (k) { 96 | if (k === 0) return 0; 97 | if (k === 1) return 1; 98 | if ((k *= 2) < 1) return 0.5 * Math.pow(1024, k - 1); 99 | return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2); 100 | }; 101 | 102 | const CircularIn = function (k) { 103 | return 1 - Math.sqrt(1 - k * k); 104 | }; 105 | 106 | const CircularOut = function (k) { 107 | return Math.sqrt(1 - (--k * k)); 108 | }; 109 | 110 | const CircularInOut = function (k) { 111 | if ((k *= 2) < 1) return -0.5 * (Math.sqrt(1 - k * k) - 1); 112 | return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); 113 | }; 114 | 115 | const ElasticIn = function (k) { 116 | const p = 0.4; 117 | let s, a = 0.1; 118 | if (k === 0) return 0; 119 | if (k === 1) return 1; 120 | if (!a || a < 1) { 121 | a = 1; s = p / 4; 122 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 123 | return -(a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 124 | }; 125 | 126 | const ElasticOut = function (k) { 127 | const p = 0.4; 128 | let s, a = 0.1; 129 | if (k === 0) return 0; 130 | if (k === 1) return 1; 131 | if (!a || a < 1) { 132 | a = 1; s = p / 4; 133 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 134 | return (a * Math.pow(2, -10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1); 135 | }; 136 | 137 | const ElasticInOut = function (k) { 138 | const p = 0.4; 139 | let s, a = 0.1; 140 | if (k === 0) return 0; 141 | if (k === 1) return 1; 142 | if (!a || a < 1) { 143 | a = 1; s = p / 4; 144 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 145 | if ((k *= 2) < 1) return -0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 146 | return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; 147 | }; 148 | 149 | const BackIn = function (k) { 150 | const s = 1.70158; 151 | return k * k * ((s + 1) * k - s); 152 | }; 153 | 154 | const BackOut = function (k) { 155 | const s = 1.70158; 156 | return --k * k * ((s + 1) * k + s) + 1; 157 | }; 158 | 159 | const BackInOut = function (k) { 160 | const s = 1.70158 * 1.525; 161 | if ((k *= 2) < 1) return 0.5 * (k * k * ((s + 1) * k - s)); 162 | return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); 163 | }; 164 | 165 | const BounceOut = function (k) { 166 | if (k < (1 / 2.75)) { 167 | return 7.5625 * k * k; 168 | } else if (k < (2 / 2.75)) { 169 | return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; 170 | } else if (k < (2.5 / 2.75)) { 171 | return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; 172 | } 173 | return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; 174 | 175 | }; 176 | 177 | const BounceIn = function (k) { 178 | return 1 - BounceOut(1 - k); 179 | }; 180 | 181 | const BounceInOut = function (k) { 182 | if (k < 0.5) return BounceIn(k * 2) * 0.5; 183 | return BounceOut(k * 2 - 1) * 0.5 + 0.5; 184 | }; 185 | 186 | var easing = /*#__PURE__*/Object.freeze({ 187 | __proto__: null, 188 | BackIn: BackIn, 189 | BackInOut: BackInOut, 190 | BackOut: BackOut, 191 | BounceIn: BounceIn, 192 | BounceInOut: BounceInOut, 193 | BounceOut: BounceOut, 194 | CircularIn: CircularIn, 195 | CircularInOut: CircularInOut, 196 | CircularOut: CircularOut, 197 | CubicIn: CubicIn, 198 | CubicInOut: CubicInOut, 199 | CubicOut: CubicOut, 200 | ElasticIn: ElasticIn, 201 | ElasticInOut: ElasticInOut, 202 | ElasticOut: ElasticOut, 203 | ExponentialIn: ExponentialIn, 204 | ExponentialInOut: ExponentialInOut, 205 | ExponentialOut: ExponentialOut, 206 | Linear: Linear, 207 | QuadraticIn: QuadraticIn, 208 | QuadraticInOut: QuadraticInOut, 209 | QuadraticOut: QuadraticOut, 210 | QuarticIn: QuarticIn, 211 | QuarticInOut: QuarticInOut, 212 | QuarticOut: QuarticOut, 213 | QuinticIn: QuinticIn, 214 | QuinticInOut: QuinticInOut, 215 | QuinticOut: QuinticOut, 216 | SineIn: SineIn, 217 | SineInOut: SineInOut, 218 | SineOut: SineOut 219 | }); 220 | 221 | /** @import { Tween } from "./tween.js" */ 222 | 223 | /** 224 | * @name TweenManager 225 | * @description Handles updating tweens 226 | */ 227 | class TweenManager { 228 | /** 229 | * @private 230 | * @type {Tween[]} 231 | */ 232 | _tweens = []; 233 | 234 | /** 235 | * @private 236 | * @type {Tween[]} 237 | */ 238 | _add = []; 239 | 240 | /** 241 | * Adds a tween 242 | * @param {Tween} tween - The tween instance to manage 243 | * @returns {Tween} - The tween instance for chaining 244 | */ 245 | add(tween) { 246 | this._add.push(tween); 247 | return tween; 248 | } 249 | 250 | /** 251 | * Update the tween 252 | * @param {number} dt - The delta time 253 | */ 254 | update(dt) { 255 | let i = 0; 256 | let n = this._tweens.length; 257 | while (i < n) { 258 | if (this._tweens[i].update(dt)) { 259 | i++; 260 | } else { 261 | this._tweens.splice(i, 1); 262 | n--; 263 | } 264 | } 265 | 266 | // add any tweens that were added mid-update 267 | if (this._add.length) { 268 | for (let i = 0; i < this._add.length; i++) { 269 | if (this._tweens.indexOf(this._add[i]) > -1) continue; 270 | this._tweens.push(this._add[i]); 271 | } 272 | this._add.length = 0; 273 | } 274 | } 275 | } 276 | 277 | /** @import { TweenManager } from "./tween-manager.js" */ 278 | /** @import { Entity } from "playcanvas" */ 279 | 280 | class Tween extends playcanvas.EventHandler { 281 | /** 282 | * @name Tween 283 | * @param {object} target - The target property that will be tweened 284 | * @param {TweenManager} manager - The tween manager 285 | * @param {Entity} entity - The Entity whose property we are tween-ing 286 | */ 287 | constructor(target, manager, entity) { 288 | 289 | super(); 290 | 291 | this.manager = manager; 292 | 293 | if (entity) { 294 | this.entity = null; // if present the tween will dirty the transforms after modify the target 295 | } 296 | 297 | this.time = 0; 298 | 299 | this.complete = false; 300 | this.playing = false; 301 | this.stopped = true; 302 | this.pending = false; 303 | 304 | this.target = target; 305 | 306 | this.duration = 0; 307 | this._currentDelay = 0; 308 | this.timeScale = 1; 309 | this._reverse = false; 310 | 311 | this._delay = 0; 312 | this._yoyo = false; 313 | 314 | this._count = 0; 315 | this._numRepeats = 0; 316 | this._repeatDelay = 0; 317 | 318 | this._from = false; // indicates a "from" tween 319 | 320 | // for rotation tween 321 | this._slerp = false; // indicates a rotation tween 322 | this._fromQuat = new playcanvas.Quat(); 323 | this._toQuat = new playcanvas.Quat(); 324 | this._quat = new playcanvas.Quat(); 325 | 326 | this.easing = Linear; 327 | 328 | this._sv = {}; // start values 329 | this._ev = {}; // end values 330 | } 331 | 332 | _parseProperties(properties) { 333 | let _properties; 334 | if (properties instanceof playcanvas.Vec2) { 335 | _properties = { 336 | x: properties.x, 337 | y: properties.y 338 | }; 339 | } else if (properties instanceof playcanvas.Vec3) { 340 | _properties = { 341 | x: properties.x, 342 | y: properties.y, 343 | z: properties.z 344 | }; 345 | } else if (properties instanceof playcanvas.Vec4) { 346 | _properties = { 347 | x: properties.x, 348 | y: properties.y, 349 | z: properties.z, 350 | w: properties.w 351 | }; 352 | } else if (properties instanceof playcanvas.Quat) { 353 | _properties = { 354 | x: properties.x, 355 | y: properties.y, 356 | z: properties.z, 357 | w: properties.w 358 | }; 359 | } else if (properties instanceof playcanvas.Color) { 360 | _properties = { 361 | r: properties.r, 362 | g: properties.g, 363 | b: properties.b 364 | }; 365 | if (properties.a !== undefined) { 366 | _properties.a = properties.a; 367 | } 368 | } else { 369 | _properties = properties; 370 | } 371 | return _properties; 372 | } 373 | 374 | 375 | // properties - js obj of values to update in target 376 | to(properties, duration, easing, delay, repeat, yoyo) { 377 | this._properties = this._parseProperties(properties); 378 | this.duration = duration; 379 | 380 | if (easing) this.easing = easing; 381 | if (delay) { 382 | this.delay(delay); 383 | } 384 | if (repeat) { 385 | this.repeat(repeat); 386 | } 387 | 388 | if (yoyo) { 389 | this.yoyo(yoyo); 390 | } 391 | 392 | return this; 393 | } 394 | 395 | from(properties, duration, easing, delay, repeat, yoyo) { 396 | this._properties = this._parseProperties(properties); 397 | this.duration = duration; 398 | 399 | if (easing) this.easing = easing; 400 | if (delay) { 401 | this.delay(delay); 402 | } 403 | if (repeat) { 404 | this.repeat(repeat); 405 | } 406 | 407 | if (yoyo) { 408 | this.yoyo(yoyo); 409 | } 410 | 411 | this._from = true; 412 | 413 | return this; 414 | } 415 | 416 | rotate(properties, duration, easing, delay, repeat, yoyo) { 417 | this._properties = this._parseProperties(properties); 418 | 419 | this.duration = duration; 420 | 421 | if (easing) this.easing = easing; 422 | if (delay) { 423 | this.delay(delay); 424 | } 425 | if (repeat) { 426 | this.repeat(repeat); 427 | } 428 | 429 | if (yoyo) { 430 | this.yoyo(yoyo); 431 | } 432 | 433 | this._slerp = true; 434 | 435 | return this; 436 | } 437 | 438 | start() { 439 | let prop, _x, _y, _z; 440 | 441 | this.playing = true; 442 | this.complete = false; 443 | this.stopped = false; 444 | this._count = 0; 445 | this.pending = (this._delay > 0); 446 | 447 | if (this._reverse && !this.pending) { 448 | this.time = this.duration; 449 | } else { 450 | this.time = 0; 451 | } 452 | 453 | if (this._from) { 454 | for (prop in this._properties) { 455 | if (this._properties.hasOwnProperty(prop)) { 456 | this._sv[prop] = this._properties[prop]; 457 | this._ev[prop] = this.target[prop]; 458 | } 459 | } 460 | 461 | if (this._slerp) { 462 | this._toQuat.setFromEulerAngles(this.target.x, this.target.y, this.target.z); 463 | 464 | _x = this._properties.x !== undefined ? this._properties.x : this.target.x; 465 | _y = this._properties.y !== undefined ? this._properties.y : this.target.y; 466 | _z = this._properties.z !== undefined ? this._properties.z : this.target.z; 467 | this._fromQuat.setFromEulerAngles(_x, _y, _z); 468 | } 469 | } else { 470 | for (prop in this._properties) { 471 | if (this._properties.hasOwnProperty(prop)) { 472 | this._sv[prop] = this.target[prop]; 473 | this._ev[prop] = this._properties[prop]; 474 | } 475 | } 476 | 477 | if (this._slerp) { 478 | _x = this._properties.x !== undefined ? this._properties.x : this.target.x; 479 | _y = this._properties.y !== undefined ? this._properties.y : this.target.y; 480 | _z = this._properties.z !== undefined ? this._properties.z : this.target.z; 481 | 482 | if (this._properties.w !== undefined) { 483 | this._fromQuat.copy(this.target); 484 | this._toQuat.set(_x, _y, _z, this._properties.w); 485 | } else { 486 | this._fromQuat.setFromEulerAngles(this.target.x, this.target.y, this.target.z); 487 | this._toQuat.setFromEulerAngles(_x, _y, _z); 488 | } 489 | } 490 | } 491 | 492 | // set delay 493 | this._currentDelay = this._delay; 494 | 495 | // add to manager when started 496 | this.manager.add(this); 497 | 498 | return this; 499 | } 500 | 501 | pause() { 502 | this.playing = false; 503 | } 504 | 505 | resume() { 506 | this.playing = true; 507 | } 508 | 509 | stop() { 510 | this.playing = false; 511 | this.stopped = true; 512 | } 513 | 514 | delay(delay) { 515 | this._delay = delay; 516 | this.pending = true; 517 | 518 | return this; 519 | } 520 | 521 | repeat(num, delay) { 522 | this._count = 0; 523 | this._numRepeats = num; 524 | if (delay) { 525 | this._repeatDelay = delay; 526 | } else { 527 | this._repeatDelay = 0; 528 | } 529 | 530 | return this; 531 | } 532 | 533 | loop(loop) { 534 | if (loop) { 535 | this._count = 0; 536 | this._numRepeats = Infinity; 537 | } else { 538 | this._numRepeats = 0; 539 | } 540 | 541 | return this; 542 | } 543 | 544 | yoyo(yoyo) { 545 | this._yoyo = yoyo; 546 | return this; 547 | } 548 | 549 | reverse() { 550 | this._reverse = !this._reverse; 551 | 552 | return this; 553 | } 554 | 555 | chain() { 556 | let n = arguments.length; 557 | 558 | while (n--) { 559 | if (n > 0) { 560 | arguments[n - 1]._chained = arguments[n]; 561 | } else { 562 | this._chained = arguments[n]; 563 | } 564 | } 565 | 566 | return this; 567 | } 568 | 569 | onUpdate(callback) { 570 | this.on('update', callback); 571 | return this; 572 | } 573 | 574 | onComplete(callback) { 575 | this.on('complete', callback); 576 | return this; 577 | } 578 | 579 | onLoop(callback) { 580 | this.on('loop', callback); 581 | return this; 582 | } 583 | 584 | update(dt) { 585 | if (this.stopped) return false; 586 | 587 | if (!this.playing) return true; 588 | 589 | if (!this._reverse || this.pending) { 590 | this.time += dt * this.timeScale; 591 | } else { 592 | this.time -= dt * this.timeScale; 593 | } 594 | 595 | // delay start if required 596 | if (this.pending) { 597 | if (this.time > this._currentDelay) { 598 | if (this._reverse) { 599 | this.time = this.duration - (this.time - this._currentDelay); 600 | } else { 601 | this.time -= this._currentDelay; 602 | } 603 | this.pending = false; 604 | } else { 605 | return true; 606 | } 607 | } 608 | 609 | let _extra = 0; 610 | if ((!this._reverse && this.time > this.duration) || (this._reverse && this.time < 0)) { 611 | this._count++; 612 | this.complete = true; 613 | this.playing = false; 614 | if (this._reverse) { 615 | _extra = this.duration - this.time; 616 | this.time = 0; 617 | } else { 618 | _extra = this.time - this.duration; 619 | this.time = this.duration; 620 | } 621 | } 622 | 623 | const elapsed = (this.duration === 0) ? 1 : (this.time / this.duration); 624 | 625 | // run easing 626 | const a = this.easing(elapsed); 627 | 628 | // increment property 629 | let s, e; 630 | for (const prop in this._properties) { 631 | if (this._properties.hasOwnProperty(prop)) { 632 | s = this._sv[prop]; 633 | e = this._ev[prop]; 634 | this.target[prop] = s + (e - s) * a; 635 | } 636 | } 637 | 638 | if (this._slerp) { 639 | this._quat.slerp(this._fromQuat, this._toQuat, a); 640 | } 641 | 642 | // if this is a entity property then we should dirty the transform 643 | if (this.entity) { 644 | this.entity._dirtifyLocal(); 645 | 646 | // apply element property changes 647 | if (this.element && this.entity.element) { 648 | this.entity.element[this.element] = this.target; 649 | } 650 | 651 | if (this._slerp) { 652 | this.entity.setLocalRotation(this._quat); 653 | } 654 | } 655 | 656 | this.fire('update', dt); 657 | 658 | if (this.complete) { 659 | const repeat = this._repeat(_extra); 660 | if (!repeat) { 661 | this.fire('complete', _extra); 662 | if (this.entity) { 663 | this.entity.off('destroy', this.stop, this); 664 | } 665 | if (this._chained) this._chained.start(); 666 | } else { 667 | this.fire('loop'); 668 | } 669 | 670 | return repeat; 671 | } 672 | 673 | return true; 674 | } 675 | 676 | _repeat(extra) { 677 | // test for repeat conditions 678 | if (this._count < this._numRepeats) { 679 | // do a repeat 680 | if (this._reverse) { 681 | this.time = this.duration - extra; 682 | } else { 683 | this.time = extra; // include overspill time 684 | } 685 | this.complete = false; 686 | this.playing = true; 687 | 688 | this._currentDelay = this._repeatDelay; 689 | this.pending = true; 690 | 691 | if (this._yoyo) { 692 | // swap start/end properties 693 | for (const prop in this._properties) { 694 | const tmp = this._sv[prop]; 695 | this._sv[prop] = this._ev[prop]; 696 | this._ev[prop] = tmp; 697 | } 698 | 699 | if (this._slerp) { 700 | this._quat.copy(this._fromQuat); 701 | this._fromQuat.copy(this._toQuat); 702 | this._toQuat.copy(this._quat); 703 | } 704 | } 705 | 706 | return true; 707 | } 708 | return false; 709 | } 710 | } 711 | 712 | const managers = new Map(); 713 | 714 | /** 715 | * Registers a tween manager with a playcanvas application, so that it will update with the 716 | * applications frame update. 717 | * 718 | * @param {AppBase} app - The playcanvas application to register with the Tween Manager 719 | * @returns {TweenManager} - The registered TweenManager 720 | */ 721 | const getTweenManager = (app) => { 722 | 723 | if (!app || !(app instanceof playcanvas.AppBase)) { 724 | throw new Error('`getTweenManager` expects an instance of `AppBase`'); 725 | } 726 | 727 | if (!managers.has(app)) { 728 | const tweenManager = new TweenManager(); 729 | managers.set(app, tweenManager); 730 | app.on('update', (dt) => { 731 | tweenManager.update(dt); 732 | }); 733 | app.on('destroy', () => managers.delete(app)); 734 | } 735 | 736 | return managers.get(app); 737 | }; 738 | 739 | /** 740 | * Tweens an entities properties. 741 | * 742 | * @param {Entity} entity - The entity target to tween 743 | * @param {object} target - An object representing the properties to tween 744 | * @param {object} options - The tween options 745 | * @returns {Tween} - The tween instance 746 | * 747 | * @example 748 | * ``` 749 | * tweenEntity(entity, entity.getLocalPosition) 750 | * .to({x: 10, y: 0, z: 0}, 1, SineOut); 751 | * ``` 752 | */ 753 | const tweenEntity = (entity, target, options) => { 754 | 755 | const tweenManager = getTweenManager(entity._app); 756 | const tween = new Tween(target, tweenManager); 757 | tween.entity = entity; 758 | 759 | entity.once('destroy', tween.stop, tween); 760 | 761 | if (options && options.element) { 762 | // specify a element property to be updated 763 | tween.element = options.element; 764 | } 765 | 766 | return tween; 767 | }; 768 | 769 | /** 770 | * This function extends the `Entity` and `AppBase` class of PlayCanvas 771 | * with convenience methods such as `app.tween()` and `entity.tween()`. 772 | * 773 | * @param {{ AppBase, Entity }} pc - the playcanvas engine 774 | * 775 | * @example 776 | * ``` 777 | * import * as pc from 'playcanvas' 778 | * addTweenExtensions(pc) 779 | * entity.tween(); // new utility method 780 | * app.tween(pc.Color(1, 0, 0)) 781 | * ``` 782 | */ 783 | const addTweenExtensions = ({ AppBase, Entity }) => { 784 | 785 | if (!AppBase) { 786 | throw new Error('The param `addExtensions` must contain the `AppBase` class. `addExtensions({ AppBase })`'); 787 | } 788 | 789 | if (!Entity) { 790 | throw new Error('The param `addExtensions` must contain the `Entity` class. `addExtensions({ Entity })`'); 791 | } 792 | 793 | // Add pc.AppBase#tween method 794 | AppBase.prototype.tween = function (target) { 795 | const tweenManager = getTweenManager(this); 796 | return new Tween(target, tweenManager); 797 | }; 798 | 799 | // Add pc.Entity#tween method 800 | Entity.prototype.tween = function (target, options) { 801 | return tweenEntity(this, target, options); 802 | }; 803 | }; 804 | 805 | if (!globalThis.pc) { 806 | throw new Error('There is no global `pc` playcanvas object.'); 807 | } 808 | 809 | // Add the easing functions to the pc global, ie. pc.SineInOut 810 | Object.assign(globalThis.pc, easing); 811 | 812 | // Extend the Entity and AppBase prototypes with tween methods 813 | addTweenExtensions(globalThis.pc); 814 | 815 | globalThis.pc.AppBase.prototype.addTweenManager = function () { 816 | this._tweenManager = getTweenManager(this); 817 | }; 818 | 819 | })); 820 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import playcanvasConfig from '@playcanvas/eslint-config'; 2 | import globals from 'globals'; 3 | 4 | export default [ 5 | ...playcanvasConfig, 6 | { 7 | files: ['**/*.js', '**/*.mjs'], 8 | languageOptions: { 9 | ecmaVersion: 2022, 10 | sourceType: 'module', 11 | globals: { 12 | ...globals.browser, 13 | ...globals.qunit, 14 | ...globals.node, 15 | 'pc': 'readonly', 16 | 'QUnit': 'readonly' 17 | } 18 | } 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace pc { 2 | type TweenTarget = Record | Quat | Vec4 | Vec3 | Vec2; 3 | 4 | // Easing Functions 5 | export function Linear(t: number): number; 6 | export function QuadraticIn(t: number): number; 7 | export function QuadraticOut(t: number): number; 8 | export function QuadraticInOut(t: number): number; 9 | export function CubicIn(t: number): number; 10 | export function CubicOut(t: number): number; 11 | export function CubicInOut(t: number): number; 12 | export function QuarticIn(t: number): number; 13 | export function QuarticOut(t: number): number; 14 | export function QuarticInOut(t: number): number; 15 | export function QuinticIn(t: number): number; 16 | export function QuinticOut(t: number): number; 17 | export function QuinticInOut(t: number): number; 18 | export function SineIn(t: number): number; 19 | export function SineOut(t: number): number; 20 | export function SineInOut(t: number): number; 21 | export function ExponentialIn(t: number): number; 22 | export function ExponentialOut(t: number): number; 23 | export function ExponentialInOut(t: number): number; 24 | export function CircularIn(t: number): number; 25 | export function CircularOut(t: number): number; 26 | export function CircularInOut(t: number): number; 27 | export function BackIn(t: number): number; 28 | export function BackOut(t: number): number; 29 | export function BackInOut(t: number): number; 30 | export function BounceIn(t: number): number; 31 | export function BounceOut(t: number): number; 32 | export function BounceInOut(t: number): number; 33 | export function ElasticIn(t: number): number; 34 | export function ElasticOut(t: number): number; 35 | export function ElasticInOut(t: number): number; 36 | 37 | /** 38 | * @name pc.Tween 39 | * @param {object} target - The target property that will be tweened 40 | * @param {pc.TweenManager} manager - The tween manager 41 | * @param {pc.Entity} entity - The pc.Entity whose property we are tweening 42 | */ 43 | export class Tween { 44 | time: number; 45 | 46 | complete: boolean; 47 | 48 | playing: boolean; 49 | 50 | stopped: boolean; 51 | 52 | pending: boolean; 53 | 54 | target: TweenTarget; 55 | 56 | duration: number; 57 | 58 | private _currentDelay: number; 59 | 60 | timeScale: number; 61 | 62 | private _reverse: boolean; 63 | 64 | private _delay: number; 65 | 66 | private _yoyo: boolean; 67 | 68 | private _count: number; 69 | 70 | private _numRepeats: number; 71 | 72 | private _repeatDelay: number; 73 | 74 | private _from: boolean; 75 | 76 | private _slerp: boolean; 77 | 78 | private _fromQuat: Quat; 79 | 80 | private _toQuat: Quat; 81 | 82 | private _quat: Quat; 83 | 84 | easing: (t: number) => number; 85 | 86 | private _sv: TweenTarget; 87 | 88 | private _ev: TweenTarget; 89 | 90 | to: ( 91 | properties: TweenTarget, 92 | duration?: number, 93 | easing?: (t: number) => number, 94 | delay?: number, 95 | repeat?: number, 96 | yoyo?: boolean 97 | ) => Tween; 98 | 99 | from: ( 100 | properties: TweenTarget, 101 | duration?: number, 102 | easing?: (t: number) => number, 103 | delay?: number, 104 | repeat?: number, 105 | yoyo?: boolean 106 | ) => Tween; 107 | 108 | rotate: ( 109 | properties: TweenTarget, 110 | duration?: number, 111 | easing?: (t: number) => number, 112 | delay?: number, 113 | repeat?: number, 114 | yoyo?: boolean 115 | ) => Tween; 116 | 117 | start: () => Tween; 118 | 119 | pause: () => void; 120 | 121 | resume: () => void; 122 | 123 | stop: () => void; 124 | 125 | delay: (delay: number) => Tween; 126 | 127 | repeat: (num: number, delay?: number) => Tween; 128 | 129 | loop: (loop: boolean) => Tween; 130 | 131 | yoyo: (yoyo: boolean) => Tween; 132 | 133 | reverse: () => Tween; 134 | 135 | chain: (...tween: Tween[]) => Tween; 136 | 137 | update: (dt: number) => boolean; 138 | 139 | onUpdate: (callback: () => void) => Tween; 140 | 141 | onComplete: (callback: () => void) => Tween; 142 | 143 | onLoop: (callback: () => void) => Tween; 144 | } 145 | 146 | export interface TweenOptions { 147 | /** Property name of an Element component to be tweened...? */ 148 | element: string; 149 | } 150 | 151 | export interface Entity { 152 | tween: (target: TweenTarget, options?: TweenOptions) => Tween; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playcanvas-tween", 3 | "version": "1.1.0", 4 | "type": "module", 5 | "author": "PlayCanvas ", 6 | "homepage": "https://playcanvas.com", 7 | "description": "Tween library for PlayCanvas engine", 8 | "keywords": [ 9 | "playcanvas", 10 | "webgl", 11 | "tween", 12 | "animation" 13 | ], 14 | "license": "MIT", 15 | "main": "tween.js", 16 | "bugs": { 17 | "url": "https://github.com/playcanvas/playcanvas-tween/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/playcanvas/playcanvas-tween.git" 22 | }, 23 | "devDependencies": { 24 | "@playcanvas/eslint-config": "^2.0.8", 25 | "chai": "^5.1.2", 26 | "eslint": "^9.19.0", 27 | "globals": "^15.14.0", 28 | "jsdom": "^26.0.0", 29 | "mocha": "^11.1.0", 30 | "rollup": "^4.32.1" 31 | }, 32 | "scripts": { 33 | "lint": "eslint src", 34 | "test": "mocha test/**/*.test.js --timeout 10000", 35 | "build": "rollup -c rollup.config.js" 36 | }, 37 | "peerDependencies": { 38 | "playcanvas": "^2.4.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | input: 'src/index.js', 4 | external: ['playcanvas'], 5 | output: { 6 | file: 'dist/tween.mjs', 7 | format: 'esm' 8 | }, 9 | treeshake: { 10 | moduleSideEffects: false // Ensure unused exports are kept if explicitly imported 11 | } 12 | }, 13 | { 14 | input: 'src/pc-entry.js', 15 | external: ['playcanvas'], 16 | output: { 17 | file: 'dist/tween.umd.js', 18 | format: 'umd', 19 | globals: { 20 | playcanvas: 'pc' 21 | } 22 | }, 23 | treeshake: { 24 | moduleSideEffects: false // Ensure unused exports are kept if explicitly imported 25 | } 26 | } 27 | ]; 28 | -------------------------------------------------------------------------------- /src/easing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Easing methods 3 | */ 4 | 5 | export const Linear = function (k) { 6 | return k; 7 | }; 8 | 9 | export const QuadraticIn = function (k) { 10 | return k * k; 11 | }; 12 | 13 | export const QuadraticOut = function (k) { 14 | return k * (2 - k); 15 | }; 16 | 17 | export const QuadraticInOut = function (k) { 18 | if ((k *= 2) < 1) { 19 | return 0.5 * k * k; 20 | } 21 | return -0.5 * (--k * (k - 2) - 1); 22 | }; 23 | 24 | export const CubicIn = function (k) { 25 | return k * k * k; 26 | }; 27 | 28 | export const CubicOut = function (k) { 29 | return --k * k * k + 1; 30 | }; 31 | 32 | export const CubicInOut = function (k) { 33 | if ((k *= 2) < 1) return 0.5 * k * k * k; 34 | return 0.5 * ((k -= 2) * k * k + 2); 35 | }; 36 | 37 | export const QuarticIn = function (k) { 38 | return k * k * k * k; 39 | }; 40 | 41 | export const QuarticOut = function (k) { 42 | return 1 - (--k * k * k * k); 43 | }; 44 | 45 | export const QuarticInOut = function (k) { 46 | if ((k *= 2) < 1) return 0.5 * k * k * k * k; 47 | return -0.5 * ((k -= 2) * k * k * k - 2); 48 | }; 49 | 50 | export const QuinticIn = function (k) { 51 | return k * k * k * k * k; 52 | }; 53 | 54 | export const QuinticOut = function (k) { 55 | return --k * k * k * k * k + 1; 56 | }; 57 | 58 | export const QuinticInOut = function (k) { 59 | if ((k *= 2) < 1) return 0.5 * k * k * k * k * k; 60 | return 0.5 * ((k -= 2) * k * k * k * k + 2); 61 | }; 62 | 63 | export const SineIn = function (k) { 64 | if (k === 0) return 0; 65 | if (k === 1) return 1; 66 | return 1 - Math.cos(k * Math.PI / 2); 67 | }; 68 | 69 | export const SineOut = function (k) { 70 | if (k === 0) return 0; 71 | if (k === 1) return 1; 72 | return Math.sin(k * Math.PI / 2); 73 | }; 74 | 75 | export const SineInOut = function (k) { 76 | if (k === 0) return 0; 77 | if (k === 1) return 1; 78 | return 0.5 * (1 - Math.cos(Math.PI * k)); 79 | }; 80 | 81 | export const ExponentialIn = function (k) { 82 | return k === 0 ? 0 : Math.pow(1024, k - 1); 83 | }; 84 | 85 | export const ExponentialOut = function (k) { 86 | return k === 1 ? 1 : 1 - Math.pow(2, -10 * k); 87 | }; 88 | 89 | export const ExponentialInOut = function (k) { 90 | if (k === 0) return 0; 91 | if (k === 1) return 1; 92 | if ((k *= 2) < 1) return 0.5 * Math.pow(1024, k - 1); 93 | return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2); 94 | }; 95 | 96 | export const CircularIn = function (k) { 97 | return 1 - Math.sqrt(1 - k * k); 98 | }; 99 | 100 | export const CircularOut = function (k) { 101 | return Math.sqrt(1 - (--k * k)); 102 | }; 103 | 104 | export const CircularInOut = function (k) { 105 | if ((k *= 2) < 1) return -0.5 * (Math.sqrt(1 - k * k) - 1); 106 | return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); 107 | }; 108 | 109 | export const ElasticIn = function (k) { 110 | const p = 0.4; 111 | let s, a = 0.1; 112 | if (k === 0) return 0; 113 | if (k === 1) return 1; 114 | if (!a || a < 1) { 115 | a = 1; s = p / 4; 116 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 117 | return -(a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 118 | }; 119 | 120 | export const ElasticOut = function (k) { 121 | const p = 0.4; 122 | let s, a = 0.1; 123 | if (k === 0) return 0; 124 | if (k === 1) return 1; 125 | if (!a || a < 1) { 126 | a = 1; s = p / 4; 127 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 128 | return (a * Math.pow(2, -10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1); 129 | }; 130 | 131 | export const ElasticInOut = function (k) { 132 | const p = 0.4; 133 | let s, a = 0.1; 134 | if (k === 0) return 0; 135 | if (k === 1) return 1; 136 | if (!a || a < 1) { 137 | a = 1; s = p / 4; 138 | } else s = p * Math.asin(1 / a) / (2 * Math.PI); 139 | if ((k *= 2) < 1) return -0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 140 | return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; 141 | }; 142 | 143 | export const BackIn = function (k) { 144 | const s = 1.70158; 145 | return k * k * ((s + 1) * k - s); 146 | }; 147 | 148 | export const BackOut = function (k) { 149 | const s = 1.70158; 150 | return --k * k * ((s + 1) * k + s) + 1; 151 | }; 152 | 153 | export const BackInOut = function (k) { 154 | const s = 1.70158 * 1.525; 155 | if ((k *= 2) < 1) return 0.5 * (k * k * ((s + 1) * k - s)); 156 | return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); 157 | }; 158 | 159 | export const BounceOut = function (k) { 160 | if (k < (1 / 2.75)) { 161 | return 7.5625 * k * k; 162 | } else if (k < (2 / 2.75)) { 163 | return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; 164 | } else if (k < (2.5 / 2.75)) { 165 | return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; 166 | } 167 | return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; 168 | 169 | }; 170 | 171 | export const BounceIn = function (k) { 172 | return 1 - BounceOut(1 - k); 173 | }; 174 | 175 | export const BounceInOut = function (k) { 176 | if (k < 0.5) return BounceIn(k * 2) * 0.5; 177 | return BounceOut(k * 2 - 1) * 0.5 + 0.5; 178 | }; 179 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { AppBase } from 'playcanvas'; 2 | 3 | import { TweenManager } from './tween-manager.js'; 4 | import { Tween } from './tween.js'; 5 | export * from './easing.js'; 6 | 7 | const managers = new Map(); 8 | 9 | /** 10 | * Registers a tween manager with a playcanvas application, so that it will update with the 11 | * applications frame update. 12 | * 13 | * @param {AppBase} app - The playcanvas application to register with the Tween Manager 14 | * @returns {TweenManager} - The registered TweenManager 15 | */ 16 | export const getTweenManager = (app) => { 17 | 18 | if (!app || !(app instanceof AppBase)) { 19 | throw new Error('`getTweenManager` expects an instance of `AppBase`'); 20 | } 21 | 22 | if (!managers.has(app)) { 23 | const tweenManager = new TweenManager(); 24 | managers.set(app, tweenManager); 25 | app.on('update', (dt) => { 26 | tweenManager.update(dt); 27 | }); 28 | app.on('destroy', () => managers.delete(app)); 29 | } 30 | 31 | return managers.get(app); 32 | }; 33 | 34 | /** 35 | * Tweens an entities properties. 36 | * 37 | * @param {Entity} entity - The entity target to tween 38 | * @param {object} target - An object representing the properties to tween 39 | * @param {object} options - The tween options 40 | * @returns {Tween} - The tween instance 41 | * 42 | * @example 43 | * ``` 44 | * tweenEntity(entity, entity.getLocalPosition) 45 | * .to({x: 10, y: 0, z: 0}, 1, SineOut); 46 | * ``` 47 | */ 48 | export const tweenEntity = (entity, target, options) => { 49 | 50 | const tweenManager = getTweenManager(entity._app); 51 | const tween = new Tween(target, tweenManager); 52 | tween.entity = entity; 53 | 54 | entity.once('destroy', tween.stop, tween); 55 | 56 | if (options && options.element) { 57 | // specify a element property to be updated 58 | tween.element = options.element; 59 | } 60 | 61 | return tween; 62 | }; 63 | 64 | /** 65 | * This function extends the `Entity` and `AppBase` class of PlayCanvas 66 | * with convenience methods such as `app.tween()` and `entity.tween()`. 67 | * 68 | * @param {{ AppBase, Entity }} pc - the playcanvas engine 69 | * 70 | * @example 71 | * ``` 72 | * import * as pc from 'playcanvas' 73 | * addTweenExtensions(pc) 74 | * entity.tween(); // new utility method 75 | * app.tween(pc.Color(1, 0, 0)) 76 | * ``` 77 | */ 78 | export const addTweenExtensions = ({ AppBase, Entity }) => { 79 | 80 | if (!AppBase) { 81 | throw new Error('The param `addExtensions` must contain the `AppBase` class. `addExtensions({ AppBase })`'); 82 | } 83 | 84 | if (!Entity) { 85 | throw new Error('The param `addExtensions` must contain the `Entity` class. `addExtensions({ Entity })`'); 86 | } 87 | 88 | // Add pc.AppBase#tween method 89 | AppBase.prototype.tween = function (target) { 90 | const tweenManager = getTweenManager(this); 91 | return new Tween(target, tweenManager); 92 | }; 93 | 94 | // Add pc.Entity#tween method 95 | Entity.prototype.tween = function (target, options) { 96 | return tweenEntity(this, target, options); 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /src/pc-entry.js: -------------------------------------------------------------------------------- 1 | import * as easing from './easing.js'; 2 | 3 | import { addTweenExtensions, getTweenManager } from './index.js'; 4 | 5 | if (!globalThis.pc) { 6 | throw new Error('There is no global `pc` playcanvas object.'); 7 | } 8 | 9 | // Add the easing functions to the pc global, ie. pc.SineInOut 10 | Object.assign(globalThis.pc, easing); 11 | 12 | // Extend the Entity and AppBase prototypes with tween methods 13 | addTweenExtensions(globalThis.pc); 14 | 15 | globalThis.pc.AppBase.prototype.addTweenManager = function () { 16 | this._tweenManager = getTweenManager(this); 17 | }; 18 | -------------------------------------------------------------------------------- /src/tween-manager.js: -------------------------------------------------------------------------------- 1 | /** @import { Tween } from "./tween.js" */ 2 | 3 | /** 4 | * @name TweenManager 5 | * @description Handles updating tweens 6 | */ 7 | class TweenManager { 8 | /** 9 | * @private 10 | * @type {Tween[]} 11 | */ 12 | _tweens = []; 13 | 14 | /** 15 | * @private 16 | * @type {Tween[]} 17 | */ 18 | _add = []; 19 | 20 | /** 21 | * Adds a tween 22 | * @param {Tween} tween - The tween instance to manage 23 | * @returns {Tween} - The tween instance for chaining 24 | */ 25 | add(tween) { 26 | this._add.push(tween); 27 | return tween; 28 | } 29 | 30 | /** 31 | * Update the tween 32 | * @param {number} dt - The delta time 33 | */ 34 | update(dt) { 35 | let i = 0; 36 | let n = this._tweens.length; 37 | while (i < n) { 38 | if (this._tweens[i].update(dt)) { 39 | i++; 40 | } else { 41 | this._tweens.splice(i, 1); 42 | n--; 43 | } 44 | } 45 | 46 | // add any tweens that were added mid-update 47 | if (this._add.length) { 48 | for (let i = 0; i < this._add.length; i++) { 49 | if (this._tweens.indexOf(this._add[i]) > -1) continue; 50 | this._tweens.push(this._add[i]); 51 | } 52 | this._add.length = 0; 53 | } 54 | } 55 | } 56 | 57 | export { TweenManager }; 58 | -------------------------------------------------------------------------------- /src/tween.js: -------------------------------------------------------------------------------- 1 | import { EventHandler, Quat, Vec2, Vec3, Vec4, Color } from 'playcanvas'; 2 | 3 | import { Linear } from './easing.js'; 4 | 5 | /** @import { TweenManager } from "./tween-manager.js" */ 6 | /** @import { Entity } from "playcanvas" */ 7 | 8 | class Tween extends EventHandler { 9 | /** 10 | * @name Tween 11 | * @param {object} target - The target property that will be tweened 12 | * @param {TweenManager} manager - The tween manager 13 | * @param {Entity} entity - The Entity whose property we are tween-ing 14 | */ 15 | constructor(target, manager, entity) { 16 | 17 | super(); 18 | 19 | this.manager = manager; 20 | 21 | if (entity) { 22 | this.entity = null; // if present the tween will dirty the transforms after modify the target 23 | } 24 | 25 | this.time = 0; 26 | 27 | this.complete = false; 28 | this.playing = false; 29 | this.stopped = true; 30 | this.pending = false; 31 | 32 | this.target = target; 33 | 34 | this.duration = 0; 35 | this._currentDelay = 0; 36 | this.timeScale = 1; 37 | this._reverse = false; 38 | 39 | this._delay = 0; 40 | this._yoyo = false; 41 | 42 | this._count = 0; 43 | this._numRepeats = 0; 44 | this._repeatDelay = 0; 45 | 46 | this._from = false; // indicates a "from" tween 47 | 48 | // for rotation tween 49 | this._slerp = false; // indicates a rotation tween 50 | this._fromQuat = new Quat(); 51 | this._toQuat = new Quat(); 52 | this._quat = new Quat(); 53 | 54 | this.easing = Linear; 55 | 56 | this._sv = {}; // start values 57 | this._ev = {}; // end values 58 | } 59 | 60 | _parseProperties(properties) { 61 | let _properties; 62 | if (properties instanceof Vec2) { 63 | _properties = { 64 | x: properties.x, 65 | y: properties.y 66 | }; 67 | } else if (properties instanceof Vec3) { 68 | _properties = { 69 | x: properties.x, 70 | y: properties.y, 71 | z: properties.z 72 | }; 73 | } else if (properties instanceof Vec4) { 74 | _properties = { 75 | x: properties.x, 76 | y: properties.y, 77 | z: properties.z, 78 | w: properties.w 79 | }; 80 | } else if (properties instanceof Quat) { 81 | _properties = { 82 | x: properties.x, 83 | y: properties.y, 84 | z: properties.z, 85 | w: properties.w 86 | }; 87 | } else if (properties instanceof Color) { 88 | _properties = { 89 | r: properties.r, 90 | g: properties.g, 91 | b: properties.b 92 | }; 93 | if (properties.a !== undefined) { 94 | _properties.a = properties.a; 95 | } 96 | } else { 97 | _properties = properties; 98 | } 99 | return _properties; 100 | } 101 | 102 | 103 | // properties - js obj of values to update in target 104 | to(properties, duration, easing, delay, repeat, yoyo) { 105 | this._properties = this._parseProperties(properties); 106 | this.duration = duration; 107 | 108 | if (easing) this.easing = easing; 109 | if (delay) { 110 | this.delay(delay); 111 | } 112 | if (repeat) { 113 | this.repeat(repeat); 114 | } 115 | 116 | if (yoyo) { 117 | this.yoyo(yoyo); 118 | } 119 | 120 | return this; 121 | } 122 | 123 | from(properties, duration, easing, delay, repeat, yoyo) { 124 | this._properties = this._parseProperties(properties); 125 | this.duration = duration; 126 | 127 | if (easing) this.easing = easing; 128 | if (delay) { 129 | this.delay(delay); 130 | } 131 | if (repeat) { 132 | this.repeat(repeat); 133 | } 134 | 135 | if (yoyo) { 136 | this.yoyo(yoyo); 137 | } 138 | 139 | this._from = true; 140 | 141 | return this; 142 | } 143 | 144 | rotate(properties, duration, easing, delay, repeat, yoyo) { 145 | this._properties = this._parseProperties(properties); 146 | 147 | this.duration = duration; 148 | 149 | if (easing) this.easing = easing; 150 | if (delay) { 151 | this.delay(delay); 152 | } 153 | if (repeat) { 154 | this.repeat(repeat); 155 | } 156 | 157 | if (yoyo) { 158 | this.yoyo(yoyo); 159 | } 160 | 161 | this._slerp = true; 162 | 163 | return this; 164 | } 165 | 166 | start() { 167 | let prop, _x, _y, _z; 168 | 169 | this.playing = true; 170 | this.complete = false; 171 | this.stopped = false; 172 | this._count = 0; 173 | this.pending = (this._delay > 0); 174 | 175 | if (this._reverse && !this.pending) { 176 | this.time = this.duration; 177 | } else { 178 | this.time = 0; 179 | } 180 | 181 | if (this._from) { 182 | for (prop in this._properties) { 183 | if (this._properties.hasOwnProperty(prop)) { 184 | this._sv[prop] = this._properties[prop]; 185 | this._ev[prop] = this.target[prop]; 186 | } 187 | } 188 | 189 | if (this._slerp) { 190 | this._toQuat.setFromEulerAngles(this.target.x, this.target.y, this.target.z); 191 | 192 | _x = this._properties.x !== undefined ? this._properties.x : this.target.x; 193 | _y = this._properties.y !== undefined ? this._properties.y : this.target.y; 194 | _z = this._properties.z !== undefined ? this._properties.z : this.target.z; 195 | this._fromQuat.setFromEulerAngles(_x, _y, _z); 196 | } 197 | } else { 198 | for (prop in this._properties) { 199 | if (this._properties.hasOwnProperty(prop)) { 200 | this._sv[prop] = this.target[prop]; 201 | this._ev[prop] = this._properties[prop]; 202 | } 203 | } 204 | 205 | if (this._slerp) { 206 | _x = this._properties.x !== undefined ? this._properties.x : this.target.x; 207 | _y = this._properties.y !== undefined ? this._properties.y : this.target.y; 208 | _z = this._properties.z !== undefined ? this._properties.z : this.target.z; 209 | 210 | if (this._properties.w !== undefined) { 211 | this._fromQuat.copy(this.target); 212 | this._toQuat.set(_x, _y, _z, this._properties.w); 213 | } else { 214 | this._fromQuat.setFromEulerAngles(this.target.x, this.target.y, this.target.z); 215 | this._toQuat.setFromEulerAngles(_x, _y, _z); 216 | } 217 | } 218 | } 219 | 220 | // set delay 221 | this._currentDelay = this._delay; 222 | 223 | // add to manager when started 224 | this.manager.add(this); 225 | 226 | return this; 227 | } 228 | 229 | pause() { 230 | this.playing = false; 231 | } 232 | 233 | resume() { 234 | this.playing = true; 235 | } 236 | 237 | stop() { 238 | this.playing = false; 239 | this.stopped = true; 240 | } 241 | 242 | delay(delay) { 243 | this._delay = delay; 244 | this.pending = true; 245 | 246 | return this; 247 | } 248 | 249 | repeat(num, delay) { 250 | this._count = 0; 251 | this._numRepeats = num; 252 | if (delay) { 253 | this._repeatDelay = delay; 254 | } else { 255 | this._repeatDelay = 0; 256 | } 257 | 258 | return this; 259 | } 260 | 261 | loop(loop) { 262 | if (loop) { 263 | this._count = 0; 264 | this._numRepeats = Infinity; 265 | } else { 266 | this._numRepeats = 0; 267 | } 268 | 269 | return this; 270 | } 271 | 272 | yoyo(yoyo) { 273 | this._yoyo = yoyo; 274 | return this; 275 | } 276 | 277 | reverse() { 278 | this._reverse = !this._reverse; 279 | 280 | return this; 281 | } 282 | 283 | chain() { 284 | let n = arguments.length; 285 | 286 | while (n--) { 287 | if (n > 0) { 288 | arguments[n - 1]._chained = arguments[n]; 289 | } else { 290 | this._chained = arguments[n]; 291 | } 292 | } 293 | 294 | return this; 295 | } 296 | 297 | onUpdate(callback) { 298 | this.on('update', callback); 299 | return this; 300 | } 301 | 302 | onComplete(callback) { 303 | this.on('complete', callback); 304 | return this; 305 | } 306 | 307 | onLoop(callback) { 308 | this.on('loop', callback); 309 | return this; 310 | } 311 | 312 | update(dt) { 313 | if (this.stopped) return false; 314 | 315 | if (!this.playing) return true; 316 | 317 | if (!this._reverse || this.pending) { 318 | this.time += dt * this.timeScale; 319 | } else { 320 | this.time -= dt * this.timeScale; 321 | } 322 | 323 | // delay start if required 324 | if (this.pending) { 325 | if (this.time > this._currentDelay) { 326 | if (this._reverse) { 327 | this.time = this.duration - (this.time - this._currentDelay); 328 | } else { 329 | this.time -= this._currentDelay; 330 | } 331 | this.pending = false; 332 | } else { 333 | return true; 334 | } 335 | } 336 | 337 | let _extra = 0; 338 | if ((!this._reverse && this.time > this.duration) || (this._reverse && this.time < 0)) { 339 | this._count++; 340 | this.complete = true; 341 | this.playing = false; 342 | if (this._reverse) { 343 | _extra = this.duration - this.time; 344 | this.time = 0; 345 | } else { 346 | _extra = this.time - this.duration; 347 | this.time = this.duration; 348 | } 349 | } 350 | 351 | const elapsed = (this.duration === 0) ? 1 : (this.time / this.duration); 352 | 353 | // run easing 354 | const a = this.easing(elapsed); 355 | 356 | // increment property 357 | let s, e; 358 | for (const prop in this._properties) { 359 | if (this._properties.hasOwnProperty(prop)) { 360 | s = this._sv[prop]; 361 | e = this._ev[prop]; 362 | this.target[prop] = s + (e - s) * a; 363 | } 364 | } 365 | 366 | if (this._slerp) { 367 | this._quat.slerp(this._fromQuat, this._toQuat, a); 368 | } 369 | 370 | // if this is a entity property then we should dirty the transform 371 | if (this.entity) { 372 | this.entity._dirtifyLocal(); 373 | 374 | // apply element property changes 375 | if (this.element && this.entity.element) { 376 | this.entity.element[this.element] = this.target; 377 | } 378 | 379 | if (this._slerp) { 380 | this.entity.setLocalRotation(this._quat); 381 | } 382 | } 383 | 384 | this.fire('update', dt); 385 | 386 | if (this.complete) { 387 | const repeat = this._repeat(_extra); 388 | if (!repeat) { 389 | this.fire('complete', _extra); 390 | if (this.entity) { 391 | this.entity.off('destroy', this.stop, this); 392 | } 393 | if (this._chained) this._chained.start(); 394 | } else { 395 | this.fire('loop'); 396 | } 397 | 398 | return repeat; 399 | } 400 | 401 | return true; 402 | } 403 | 404 | _repeat(extra) { 405 | // test for repeat conditions 406 | if (this._count < this._numRepeats) { 407 | // do a repeat 408 | if (this._reverse) { 409 | this.time = this.duration - extra; 410 | } else { 411 | this.time = extra; // include overspill time 412 | } 413 | this.complete = false; 414 | this.playing = true; 415 | 416 | this._currentDelay = this._repeatDelay; 417 | this.pending = true; 418 | 419 | if (this._yoyo) { 420 | // swap start/end properties 421 | for (const prop in this._properties) { 422 | const tmp = this._sv[prop]; 423 | this._sv[prop] = this._ev[prop]; 424 | this._ev[prop] = tmp; 425 | } 426 | 427 | if (this._slerp) { 428 | this._quat.copy(this._fromQuat); 429 | this._fromQuat.copy(this._toQuat); 430 | this._toQuat.copy(this._quat); 431 | } 432 | } 433 | 434 | return true; 435 | } 436 | return false; 437 | } 438 | } 439 | 440 | export { Tween }; 441 | -------------------------------------------------------------------------------- /test/tween.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it, beforeEach, afterEach } from 'mocha'; 3 | import { Entity, Vec2, Vec3, Vec4 } from 'playcanvas'; 4 | import * as pc from 'playcanvas'; 5 | 6 | import { createApp } from './utils/create-app.js'; 7 | import { jsdomSetup, jsdomTeardown } from './utils/jsdom.js'; 8 | import { SineOut } from '../src/easing.js'; 9 | import { getTweenManager, addTweenExtensions } from '../src/index.js'; 10 | import { Tween } from '../src/tween.js'; 11 | 12 | 13 | describe('Tween()', () => { 14 | 15 | let app, tweenManager; 16 | 17 | beforeEach(() => { 18 | jsdomSetup(); 19 | app = createApp(); 20 | tweenManager = getTweenManager(app); 21 | app.start(); 22 | }); 23 | 24 | afterEach(() => { 25 | app?.destroy(); 26 | app = null; 27 | jsdomTeardown(); 28 | }); 29 | 30 | it('Tween Vec4 to Vec4', (done) => { 31 | const e = new Entity(); 32 | app.root.addChild(e); 33 | 34 | const value = new Vec4(); 35 | const target = new Vec4(10, 10, 10, 10); 36 | 37 | const tween = new Tween(value, tweenManager); 38 | tween.to(target, 0.1, SineOut); 39 | tween.onComplete(() => { 40 | expect(value.y).to.equal(10); 41 | expect(value.z).to.equal(10); 42 | expect(value.w).to.equal(10); 43 | done(); 44 | }); 45 | tween.start(); 46 | }); 47 | 48 | it('Tween Vec3 to Vec3', (done) => { 49 | const e = new Entity(); 50 | app.root.addChild(e); 51 | 52 | const value = new Vec3(); 53 | const target = new Vec3(10, 10, 10); 54 | 55 | const tween = new Tween(value, tweenManager); 56 | tween.to(target, 0.1, SineOut) 57 | .onComplete(() => { 58 | expect(value.x).to.equal(10); 59 | expect(value.y).to.equal(10); 60 | expect(value.z).to.equal(10); 61 | done(); 62 | }) 63 | .start(); 64 | }); 65 | 66 | it('Tween Vec2 to Vec2', (done) => { 67 | const e = new Entity(); 68 | app.root.addChild(e); 69 | 70 | const value = new Vec2(); 71 | const target = new Vec2(10, 10); 72 | 73 | const tween = new Tween(value, tweenManager); 74 | tween.to(target, 0.1, SineOut) 75 | .onComplete(() => { 76 | expect(value.x).to.equal(10); 77 | expect(value.y).to.equal(10); 78 | done(); 79 | }) 80 | .start(); 81 | }); 82 | }); 83 | 84 | describe('entity.tween()', () => { 85 | 86 | let app; 87 | addTweenExtensions(pc); 88 | 89 | beforeEach(() => { 90 | jsdomSetup(); 91 | app = createApp(); 92 | app.start(); 93 | }); 94 | 95 | afterEach(() => { 96 | app?.destroy(); 97 | app = null; 98 | jsdomTeardown(); 99 | }); 100 | 101 | it('entity tween getLocalPosition.x', (done) => { 102 | const e = new Entity(); 103 | app.root.addChild(e); 104 | e.tween(e.getLocalPosition()) 105 | .to({ x: 10 }, 0.1, SineOut) 106 | .start() 107 | .onComplete(() => { 108 | expect(e.getLocalPosition().x).to.equal(10); 109 | e.destroy(); 110 | done(); 111 | }); 112 | }); 113 | 114 | it('entity tween localPosition.x', (done) => { 115 | const e = new Entity(); 116 | app.root.addChild(e); 117 | 118 | e.tween(e.localPosition) 119 | .to({ x: 10 }, 0.1, SineOut) 120 | .start() 121 | .onComplete(() => { 122 | expect(e.getLocalPosition().x).to.equal(10); 123 | e.destroy(); 124 | done(); 125 | }); 126 | }); 127 | 128 | it('entity tween getLocalPosition to Vec3', (done) => { 129 | const e = new Entity(); 130 | app.root.addChild(e); 131 | 132 | const target = new Vec3(10, 10, 10); 133 | 134 | e.tween(e.getLocalPosition()) 135 | .to(target, 0.1, SineOut) 136 | .start() 137 | .onComplete(() => { 138 | expect(e.getLocalPosition().x).to.equal(10); 139 | expect(e.getLocalPosition().y).to.equal(10); 140 | expect(e.getLocalPosition().z).to.equal(10); 141 | e.destroy(); 142 | done(); 143 | }); 144 | }); 145 | 146 | it('entity tween element color', (done) => { 147 | const e = new Entity(); 148 | e.addComponent('element', { 149 | type: 'image' 150 | }); 151 | app.root.addChild(e); 152 | 153 | const target = new pc.Color(0.5, 0.5, 0.5, 0.5); 154 | 155 | e.tween(e.element.color, { element: 'color' }) 156 | .to(target, 0.1, SineOut) 157 | .start() 158 | .onComplete(() => { 159 | expect(e.element.color.r).to.equal(0.5); 160 | expect(e.element.color.g).to.equal(0.5); 161 | expect(e.element.color.b).to.equal(0.5); 162 | expect(e.element.color.a).to.equal(0.5); 163 | e.destroy(); 164 | done(); 165 | }); 166 | }); 167 | 168 | it('entity tween.rotate getLocalEulerAngles', (done) => { 169 | const e = new Entity(); 170 | e.addComponent('element', { 171 | type: 'image' 172 | }); 173 | app.root.addChild(e); 174 | 175 | const target = new pc.Vec3(0, 0, 180); 176 | 177 | e.tween(e.getLocalEulerAngles()) 178 | .rotate(target, 0.1, SineOut) 179 | .start() 180 | .onComplete(() => { 181 | expect(e.getLocalEulerAngles().x).to.equal(target.x); 182 | expect(e.getLocalEulerAngles().y).to.equal(target.y); 183 | expect(e.getLocalEulerAngles().z).to.equal(target.z); 184 | e.destroy(); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/utils/create-app.js: -------------------------------------------------------------------------------- 1 | import { Application, NullGraphicsDevice } from 'playcanvas'; 2 | 3 | /** 4 | * Create a new application instance that uses the null graphics device. 5 | * @returns {Application} The new application instance. 6 | */ 7 | function createApp() { 8 | const canvas = document.createElement('canvas'); 9 | const graphicsDevice = new NullGraphicsDevice(canvas); 10 | return new Application(canvas, { graphicsDevice }); 11 | } 12 | 13 | export { createApp }; 14 | -------------------------------------------------------------------------------- /test/utils/jsdom.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import * as pc from 'playcanvas'; 3 | 4 | let jsdom; 5 | 6 | export const jsdomSetup = () => { 7 | const html = ''; 8 | 9 | jsdom = new JSDOM(html, { 10 | resources: 'usable', // Allow the engine to load assets 11 | runScripts: 'dangerously', // Allow the engine to run scripts 12 | url: 'http://localhost:3000' // Set the URL of the document 13 | }); 14 | 15 | // Copy the window and document to global scope 16 | global.window = jsdom.window; 17 | global.document = jsdom.window.document; 18 | global.navigator = jsdom.window.navigator; 19 | 20 | pc.platform.browser = true; 21 | 22 | // Copy the DOM APIs used by the engine to global scope 23 | global.ArrayBuffer = jsdom.window.ArrayBuffer; 24 | global.Audio = jsdom.window.Audio; 25 | global.DataView = jsdom.window.DataView; 26 | global.Image = jsdom.window.Image; 27 | global.KeyboardEvent = jsdom.window.KeyboardEvent; 28 | global.MouseEvent = jsdom.window.MouseEvent; 29 | global.XMLHttpRequest = jsdom.window.XMLHttpRequest; 30 | 31 | const requestAnimationFrameQueue = new Map(); 32 | let requestAnimationFrameId = 0; 33 | 34 | global.requestAnimationFrame = function (callback) { 35 | const id = ++requestAnimationFrameId; 36 | requestAnimationFrameQueue.set(id, callback); 37 | 38 | setTimeout(() => { 39 | if (requestAnimationFrameQueue.has(id)) { 40 | requestAnimationFrameQueue.get(id)(Date.now()); 41 | requestAnimationFrameQueue.delete(id); 42 | } 43 | }, 16); // Approximate 60FPS (16.67ms per frame) 44 | 45 | return id; 46 | }; 47 | 48 | const cancelAnimationFrame = function (id) { 49 | requestAnimationFrameQueue.delete(id); 50 | }; 51 | 52 | global.cancelAnimationFrame = global.window.cancelAnimationFrame = cancelAnimationFrame; 53 | 54 | // Copy the PlayCanvas API to global scope (only required for 'classic' scripts) 55 | jsdom.window.pc = pc; 56 | }; 57 | 58 | export const jsdomTeardown = () => { 59 | jsdom = null; 60 | }; 61 | --------------------------------------------------------------------------------