Skip to content
This repository has been archived by the owner on Apr 2, 2023. It is now read-only.

Version 2 plans #1

Open
sole opened this issue Jun 21, 2016 · 6 comments
Open

Version 2 plans #1

sole opened this issue Jun 21, 2016 · 6 comments

Comments

@sole
Copy link
Member

sole commented Jun 21, 2016

(or: why sole is afraid of getting too close to the current code)

The first release of this library was sometime around 2010. This is ancient in JS terms! So much has changed, so many people have participated on it. It's used by many but maintained by few. This has to change.

The amount of bugs and issues is way more than I can deal with, and one of the big reasons for that is that I do not understand all the code that I didn't write. There are missing tests, and the interactions between some features are... quite unexpected sometimes.

Also, the way things are done on the web is different now. We have node.js modules and build processses, we have ES6 and better JavaScript, we have getters and setters (which tween does not support for some reason), there are CSS animations, etc etc... People often just want to use the easing equations, but they have to include the whole library. Etc.

There were talks for a "es6" version of the library but it never took off for various reasons (here's a repo for it).

As it stands right now, I don't think an ES6 version is attractive per se - the tooling around ES6 to ES5 is quite volatile, and I can't find immediate useful advantages of rewriting in ES6, but what would be really interesting is to start a V2 that is built out of other modules. Each module should be a different project and github repository, published to npm individually. Then "Tween-v2" should be built by requiring those modules.

An structure could be this:

  • Tween-v2
    • Tween Manager (the current TWEEN object)
    • Tween (the current TWEEN.Tween)
    • Easings (the current easings)

I believe this would be a good compromise between trying to adding new stuff into the existing single src/Tween.js file, which is getting really unwieldly, and rewriting everything with a new syntax and having to add lots of slow build steps to eventually end up transpiling into ES5 because no browser implements ES6 modules yet.

Another advantage is that we would enter this mindset of splitting things in modules and get used to building Tween or importing parts of it Tween just as we need them, instead of piling all into The Code Monolith. For example, we could have a project that simplifies animating CSS properties by adding 'px' etc (which the current version doesn't). This would not be used by everyone, hence it would live on a different repository.

It makes it easier for people who want to contribute with new features, as maintainers do not need to be super worried that a new addition is going to break the core functionality.

I am inclined to think that we do not need a new "future" tween.js project repository. We could probably start prototyping this new version in a new branch and publish to npm with a 'next' kind of tag if people want to play with it. The most important stuff would be actually in other modules anyway. When we consider the new version solid enough we can merge it back to master, and if someone needs an old version for compatibility, there will always be an archive.

Thoughts? Enthusiasm? Who wants to jump on board? Everyone welcome! 🙋🏻

@trusktr
Copy link
Member

trusktr commented Jun 22, 2016

  • Tween-v2
    • Tween Manager (the current TWEEN object)
    • Tween (the current TWEEN.Tween)
    • Easings (the current easings)

I like that idea. By the way, since we're on the topic of a new version, how do you feel about Famous 0.3 Transitionable pattern? Example here: http://deprecated.famous.org/university/lessons/#/famous-102/transitionables/1

The idea would be something like (slightly modified from the Famous example, renamed to Tween):

import sleep from 'awaitbox/timers/sleep'
import Tween from 'tween.js/Tween' // v2

async function app() {

    let tween = new Tween(0) // starting value
    tween.to(100, {duration: 2000, curve: 'ExpoIn'})

    // start tween
    requestAnimationFrame(function loop(time) {
      console.log(tween.get(time))
      if (tween.isRunning) requestAnimationFrame(loop)
    })

    // delay for a sec
    await sleep(1000)

    tween.pause() // causes current animation loop to stop.

    // delay for another sec
    await sleep(1000)

    // start new loop to unpause.
    requestAnimationFrame(function loop(time) {
      console.log(tween.get(time))
      if (tween.isRunning) requestAnimationFrame(loop)
    })

    await sleep(500)
    tween.stop() // stop a half second early.
}

app()

Maybe the .get() method would auto-start the transition the first time it is called, with optional time parameter, otherwise uses performance.now().

isRunning is true when the transitionable has been started and is not paused. isStarted is true when the transitionable has been started (first .get() call), even if paused.

States (assume the conditional expressions are truthy):

  • started, not paused.
    • isRunning === true
    • isStarted === true
    • isComplete === false
  • started, but paused with .pause() call.
    • isRunning === false
    • isStarted === true
    • isComplete === false
  • stopped (not started)
    • isRunning === false
    • isStarted === false, current value is set back to starting value on .stop() call
    • isComplete === false
  • complete (started, and finished tweening)
    • isRunning === false
    • isStarted === false, current value is set back to starting value on .stop() call
    • isComplete === true

In all cases, the following two booleans are composed:

  • isPaused = !isRunning && isStarted && !isComplete
  • isStopped = !isRunning && !isStarted && !isComplete

The paused state can only be true if the Transitionable was started already, otherwise isPaused is false if the Transitionable was not started yet.

Custom easing functions can be supplied:

rotationTransition.to(100, {duration: 1000, curve: customFunction})

So far, that covers the Tween class and custom functions (what should the signature for custom tween functions be?)

As for a tween manager, maybe that part isn't necessary. Seems like it is difficult to guess people's use cases. For example, in v1, TWEEN.update(time) could be called from two different loops, in two separate components, causing double CPU usage (one component might only need values from a subset of tweens, while another component needs values from a different set, yet TWEEN.update() updates all tweens.

If a tween manager class is made, maybe components would instantiate instances of it, so updating it simply calls the .get() of all tweens registered only by given component. Tween might have a value getter:

import sleep from 'awaitbox/timers/sleep'
import TweenManager from 'tween.js/TweenManager' // v2

import {tween1, tween2, tween3, tween4} from 'somewhere'

async function app() {

    // pass tweens into constructor.
    let tweens = new TweenManager([ tween1, tween2, tween3 ])

    // inside an animation loop...
    requestAnimationFrame(function loop() {

        // updates only tweens that are started and not paused and not complete.
        tweens.update(time)

        console.log(tween1.value)
        console.log(tween2.value)
        console.log(tween3.value)
        console.log(tween4.value)

        // if any of them managed tweens are running.
        if (tweens.hasRunning) requestAnimationFrame(loop)
    })

    await sleep(1000)

    // pass to add method later.
    tweens.add(tween4)

    await sleep(1000)

    // ability to remove them
    tweens.remove(tween3)
}

app()

TweenManager might have booleans too:

  • hasRunning - if any tweens are running
  • allRunning - if all tweens are running
  • hasComplete - if any are complete
  • allComplete - if all are complete
  • allStarted - if any are started
  • hasStarted - if all are started
  • other booleans?

@dalisoft
Copy link

import Tween from './tweenjs/src/core.js';
import Easing from './tweenjs/dist/easing.js';
import GroupTween from './tweenjs/dist/group.js';
import SleepWake from './tweenjs/dist/powersaver.js';
import Interpolation from './tweenjs/dist/interpolation.js';

let tween1 = Tween.get({x:50,y:50}).duration('2s').end({x:0,y:100});
let tween2 = Tween.get({z:5,a:0}).duration('1.8s').end({z:3,a:0.5});
let tweenGroup = new GroupTween();
tweenGroup.add(tween1).add(tween2).set({tween2:{Interpolation:Interpolation.Bezier}});
tweenGroup.start();

SleepWake.change(function(status){
if (status === 'wake') {
tweenGroup.wake();
} else {
tweenGroup.sleep(); // power saving mode to save battery, especially on mobile device
}
});

@sole, Do you like syntax i writed.

  • SleepWake does visiblity change and corrects time, because when grouping/sequencing tweens, timing goes wrong when visiblity changes

@usefulthink
Copy link

Thoughts? Enthusiasm? Who wants to jump on board? Everyone welcome! 🙋🏻

Enthusiasm – YES! Want to jump on board – YES!
Thoughts – need to think about that, will post some Ideas later on.

@mikebolt
Copy link

I really like the modularity idea and I think that we need to move in that direction. However, maybe we don't need a separate repository for each module? It might help keep issues separate, but as long as the modules are all single .js files, I think keeping them in a single repository is a good option.

@trusktr I'm not sure that I understand the Transitionable pattern. In your example, you tween a single value. I hope that tweening multiple values of an object simultaneously will still be possible; I think that is one of the strongest features of tween.js.

If a tween manager class is made, maybe components would instantiate instances of it

When I created a prototype of the TweenGroup class (twice) I realized that my class was basically a flexible version of the TWEEN object. If we made a TweenManager class it would basically be the exact same as the TweenGroup class that we have proposed before, and visa versa.

I like the idea of making the tween's state inspectable. In my mind there are two different kinds of "pause":

  1. Don't update while paused. When the tween is unpaused, resume the tween at the exact value(s) it had when it was paused, regardless of how long the tween was paused.
  2. Don't update while paused. When the tween is unpaused, resume the tween by skipping ahead to the value(s) it would have had if it hadn't been paused.

I think that both kinds of pausing should be supported, but we should at least make it clear how "pause" actually behaves.

@dalisoft
Copy link

I think you're can look at here about ES6 class(ed) Tween.js

@trusktr
Copy link
Member

trusktr commented Feb 26, 2017

SleepWake.change(function(status){
  if (status === 'wake') {
    tweenGroup.wake();
  } else {
    tweenGroup.sleep(); // power saving mode to save battery, especially on mobile device
  }
});

@dalisoft I don't think this idea belongs in Tween.js. We can just start and stop a Tween or TweenManager (or GroupTween in your example) within window focus/unfocus events. Would that be enough? A separate library could help automate running logic any time an app isn't in the user's attention (sleep, display off, window unfocus, etc), but I don't think it is Tween.js' responsibility to worry about that. Tween.js should only care about starting, updating, pausing, stopping, looping, and possibly restarting tweens. With that API surface, other code can stop tweens as needed.

I hope that tweening multiple values of an object simultaneously will still be possible

@mikebolt The Famous Transitionable pattern (renamed to Tween in my examples) also supports multiple values. For example (using the name Tween instead of Transitionable),

let tween = new Tween(0) // starting value
tween.to(100, {duration: 2000, curve: 'ExpoIn'})
// ... time passes ...
console.log(tween.get(time)) // n

or

let tween = new Tween([0, 10, 5]) // multiple starting values
tween.to([100, 0, 20], {duration: 2000, curve: 'ExpoIn'})
// ... time passes ...
console.log(tween.get(time)) // [n1, n2, n3]

We could also support an additional object form (Famous Transitionable doesn't) similar to Tween.js v1:

let tween = new Tween({x:0, y:10, z:5}) // multiple starting values
tween.to({x:100, y:0, z:20}, {duration: 2000, curve: 'ExpoIn'})
// ... time passes ...
console.log(tween.get(time)) // {x:n1, y:n2, z:n3}

The third form would has the same advantage as v1 in that we can animate an object directly instead of needing to apply returned values somewhere. The object passed into .to could be a Three.js Object3D.position object, for example, and tween.get() will cause the values to be updated.

Maybe tween.next() is a better name? The name can be thought of as "applying the next values to the given object" or "getting and returning the next values", so it is meaningful in both forms of use. The name tween.get() implies only "getting and returning the next values" but not necessarily "applying the next values to the given object".

When I created a prototype of the TweenGroup class (twice) I realized that my class was basically a flexible version of the TWEEN object.

Yep, almost the same concept, except that we add Tweens and remove Tweens from separate instances rather than a single global instance. This is a great idea so that components don't hammer each other. Imagine installing a bunch of animated components from NPM and they all unintentionally updating each other's Tweens and causing unexpected behavior.

So the examples would be:

let tween = new Tween(0)
tween.to(100, {duration: 2000, curve: 'ExpoIn'})
// ... time passes ...
console.log(tween.next(time))
  1. Don't update while paused. When the tween is unpaused, resume the tween at the exact value(s) it had when it was paused, regardless of how long the tween was paused.
  2. Don't update while paused. When the tween is unpaused, resume the tween by skipping ahead to the value(s) it would have had if it hadn't been paused.

When would you want to use the second behavior?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants