|
@@ -0,0 +1,283 @@
|
|
|
+/**
|
|
|
+ * Controller class for the Timeliner GUI.
|
|
|
+ *
|
|
|
+ * Timeliner GUI library (required to use this class):
|
|
|
+ *
|
|
|
+ * ../libs/timeliner_gui.min.js
|
|
|
+ *
|
|
|
+ * Source code:
|
|
|
+ *
|
|
|
+ * https://github.com/tschw/timeliner_gui
|
|
|
+ * https://github.com/zz85/timeliner (fork's origin)
|
|
|
+ *
|
|
|
+ * @author tschw
|
|
|
+ *
|
|
|
+ */
|
|
|
+
|
|
|
+import {
|
|
|
+ AnimationClip,
|
|
|
+ AnimationMixer,
|
|
|
+ AnimationUtils,
|
|
|
+ PropertyBinding
|
|
|
+} from "../../../build/three.module.js";
|
|
|
+
|
|
|
+var TimelinerController = function TimelinerController( scene, trackInfo, onUpdate ) {
|
|
|
+
|
|
|
+ this._scene = scene;
|
|
|
+ this._trackInfo = trackInfo;
|
|
|
+
|
|
|
+ this._onUpdate = onUpdate;
|
|
|
+
|
|
|
+ this._mixer = new AnimationMixer( scene );
|
|
|
+ this._clip = null;
|
|
|
+ this._action = null;
|
|
|
+
|
|
|
+ this._tracks = {};
|
|
|
+ this._propRefs = {};
|
|
|
+ this._channelNames = [];
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+TimelinerController.prototype = {
|
|
|
+
|
|
|
+ constructor: TimelinerController,
|
|
|
+
|
|
|
+ init: function () {
|
|
|
+
|
|
|
+ var tracks = [],
|
|
|
+ trackInfo = this._trackInfo;
|
|
|
+
|
|
|
+ for ( var i = 0, n = trackInfo.length; i !== n; ++ i ) {
|
|
|
+
|
|
|
+ var spec = trackInfo[ i ];
|
|
|
+
|
|
|
+ tracks.push( this._addTrack( spec.type, spec.propertyPath, spec.initialValue, spec.interpolation ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this._clip = new AnimationClip( 'editclip', 0, tracks );
|
|
|
+ this._action = this._mixer.clipAction( this._clip ).play();
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ setDisplayTime: function ( time ) {
|
|
|
+
|
|
|
+ this._action.time = time;
|
|
|
+ this._mixer.update( 0 );
|
|
|
+
|
|
|
+ this._onUpdate();
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ setDuration: function ( duration ) {
|
|
|
+
|
|
|
+ this._clip.duration = duration;
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ getChannelNames: function () {
|
|
|
+
|
|
|
+ return this._channelNames;
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ getChannelKeyTimes: function ( channelName ) {
|
|
|
+
|
|
|
+ return this._tracks[ channelName ].times;
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ setKeyframe: function ( channelName, time ) {
|
|
|
+
|
|
|
+ var track = this._tracks[ channelName ],
|
|
|
+ times = track.times,
|
|
|
+ index = Timeliner.binarySearch( times, time ),
|
|
|
+ values = track.values,
|
|
|
+ stride = track.getValueSize(),
|
|
|
+ offset = index * stride;
|
|
|
+
|
|
|
+ if ( index < 0 ) {
|
|
|
+
|
|
|
+ // insert new keyframe
|
|
|
+
|
|
|
+ index = ~ index;
|
|
|
+ offset = index * stride;
|
|
|
+
|
|
|
+ var nTimes = times.length + 1,
|
|
|
+ nValues = values.length + stride;
|
|
|
+
|
|
|
+ for ( var i = nTimes - 1; i !== index; -- i ) {
|
|
|
+
|
|
|
+ times[ i ] = times[ i - 1 ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ for ( var i = nValues - 1, e = offset + stride - 1; i !== e; -- i ) {
|
|
|
+
|
|
|
+ values[ i ] = values[ i - stride ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ times[ index ] = time;
|
|
|
+ this._propRefs[ channelName ].getValue( values, offset );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ delKeyframe: function ( channelName, time ) {
|
|
|
+
|
|
|
+ var track = this._tracks[ channelName ],
|
|
|
+ times = track.times,
|
|
|
+ index = Timeliner.binarySearch( times, time );
|
|
|
+
|
|
|
+ // we disallow to remove the keyframe when it is the last one we have,
|
|
|
+ // since the animation system is designed to always produce a defined
|
|
|
+ // state
|
|
|
+
|
|
|
+ if ( times.length > 1 && index >= 0 ) {
|
|
|
+
|
|
|
+ var nTimes = times.length - 1,
|
|
|
+ values = track.values,
|
|
|
+ stride = track.getValueSize(),
|
|
|
+ nValues = values.length - stride;
|
|
|
+
|
|
|
+ // note: no track.getValueSize when array sizes are out of sync
|
|
|
+
|
|
|
+ for ( var i = index; i !== nTimes; ++ i ) {
|
|
|
+
|
|
|
+ times[ i ] = times[ i + 1 ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ times.pop();
|
|
|
+
|
|
|
+ for ( var offset = index * stride; offset !== nValues; ++ offset ) {
|
|
|
+
|
|
|
+ values[ offset ] = values[ offset + stride ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ values.length = nValues;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ moveKeyframe: function ( channelName, time, delta, moveRemaining ) {
|
|
|
+
|
|
|
+ var track = this._tracks[ channelName ],
|
|
|
+ times = track.times,
|
|
|
+ index = Timeliner.binarySearch( times, time );
|
|
|
+
|
|
|
+ if ( index >= 0 ) {
|
|
|
+
|
|
|
+ var endAt = moveRemaining ? times.length : index + 1,
|
|
|
+ needsSort = times[ index - 1 ] <= time ||
|
|
|
+ ! moveRemaining && time >= times[ index + 1 ];
|
|
|
+
|
|
|
+ while ( index !== endAt ) times[ index ++ ] += delta;
|
|
|
+
|
|
|
+ if ( needsSort ) this._sort( track );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ serialize: function () {
|
|
|
+
|
|
|
+ var result = {
|
|
|
+ duration: this._clip.duration,
|
|
|
+ channels: {}
|
|
|
+ },
|
|
|
+
|
|
|
+ names = this._channelNames,
|
|
|
+ tracks = this._tracks,
|
|
|
+
|
|
|
+ channels = result.channels;
|
|
|
+
|
|
|
+ for ( var i = 0, n = names.length; i !== n; ++ i ) {
|
|
|
+
|
|
|
+ var name = names[ i ],
|
|
|
+ track = tracks[ name ];
|
|
|
+
|
|
|
+ channels[ name ] = {
|
|
|
+
|
|
|
+ times: track.times,
|
|
|
+ values: track.values
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ deserialize: function ( structs ) {
|
|
|
+
|
|
|
+ var names = this._channelNames,
|
|
|
+ tracks = this._tracks,
|
|
|
+
|
|
|
+ channels = structs.channels;
|
|
|
+
|
|
|
+ this.setDuration( structs.duration );
|
|
|
+
|
|
|
+ for ( var i = 0, n = names.length; i !== n; ++ i ) {
|
|
|
+
|
|
|
+ var name = names[ i ],
|
|
|
+ track = tracks[ name ],
|
|
|
+ data = channels[ name ];
|
|
|
+
|
|
|
+ this._setArray( track.times, data.times );
|
|
|
+ this._setArray( track.values, data.values );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // update display
|
|
|
+ this.setDisplayTime( this._mixer.time );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ _sort: function ( track ) {
|
|
|
+
|
|
|
+ var times = track.times, order = AnimationUtils.getKeyframeOrder( times );
|
|
|
+
|
|
|
+ this._setArray( times, AnimationUtils.sortedArray( times, 1, order ) );
|
|
|
+
|
|
|
+ var values = track.values,
|
|
|
+ stride = track.getValueSize();
|
|
|
+
|
|
|
+ this._setArray( values, AnimationUtils.sortedArray( values, stride, order ) );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ _setArray: function ( dst, src ) {
|
|
|
+
|
|
|
+ dst.length = 0;
|
|
|
+ dst.push.apply( dst, src );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ _addTrack: function ( type, prop, initialValue, interpolation ) {
|
|
|
+
|
|
|
+ var track = new type( prop, [ 0 ], initialValue, interpolation );
|
|
|
+
|
|
|
+ // data must be in JS arrays so it can be resized
|
|
|
+ track.times = Array.prototype.slice.call( track.times );
|
|
|
+ track.values = Array.prototype.slice.call( track.values );
|
|
|
+
|
|
|
+ this._channelNames.push( prop );
|
|
|
+ this._tracks[ prop ] = track;
|
|
|
+
|
|
|
+ // for recording the state:
|
|
|
+ this._propRefs[ prop ] =
|
|
|
+ new PropertyBinding( this._scene, prop );
|
|
|
+
|
|
|
+ return track;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+export { TimelinerController };
|