/** * 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 };