|
@@ -1,75 +1,144 @@
|
|
|
/**
|
|
|
*
|
|
|
- * A Track that returns a keyframe interpolated value, currently linearly interpolated
|
|
|
+ * A timed sequence of keyframes for a specific property.
|
|
|
+ *
|
|
|
*
|
|
|
* @author Ben Houston / http://clara.io/
|
|
|
* @author David Sarno / http://lighthaus.us/
|
|
|
+ * @author tschw
|
|
|
*/
|
|
|
|
|
|
-THREE.KeyframeTrack = function ( name, keys ) {
|
|
|
+THREE.KeyframeTrack = function ( name, times, values, interpolation ) {
|
|
|
|
|
|
if( name === undefined ) throw new Error( "track name is undefined" );
|
|
|
- if( keys === undefined || keys.length === 0 ) throw new Error( "no keys in track named " + name );
|
|
|
+
|
|
|
+ if( times === undefined || times.length === 0 ||
|
|
|
+ values === undefined || values.length === 0 ) {
|
|
|
+
|
|
|
+ throw new Error( "no keyframes in track named " + name );
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
this.name = name;
|
|
|
- this.keys = keys; // time in seconds, value as value
|
|
|
|
|
|
- // the index of the last result, used as a starting point for local search.
|
|
|
- this.lastIndex = 0;
|
|
|
+ this.times = THREE.AnimationUtils.convertArray( times, this.TimeBufferType );
|
|
|
+ this.values = THREE.AnimationUtils.convertArray( values, this.ValueBufferType );
|
|
|
+
|
|
|
+ this.setInterpolation( interpolation || this.DefaultInterpolation );
|
|
|
|
|
|
this.validate();
|
|
|
this.optimize();
|
|
|
+
|
|
|
};
|
|
|
|
|
|
THREE.KeyframeTrack.prototype = {
|
|
|
|
|
|
constructor: THREE.KeyframeTrack,
|
|
|
|
|
|
- getAt: function( time ) {
|
|
|
+ TimeBufferType: Float64Array,
|
|
|
+ ValueBufferType: Float64Array,
|
|
|
|
|
|
+ DefaultInterpolation: THREE.InterpolateLinear,
|
|
|
|
|
|
- // this can not go higher than this.keys.length.
|
|
|
- while( ( this.lastIndex < this.keys.length ) && ( time >= this.keys[this.lastIndex].time ) ) {
|
|
|
- this.lastIndex ++;
|
|
|
- };
|
|
|
+ InterpolantFactoryMethodDiscrete: function( result ) {
|
|
|
|
|
|
- // this can not go lower than 0.
|
|
|
- while( ( this.lastIndex > 0 ) && ( time < this.keys[this.lastIndex - 1].time ) ) {
|
|
|
- this.lastIndex --;
|
|
|
- }
|
|
|
+ return new THREE.DiscreteInterpolant(
|
|
|
+ this.times, this.values, this.getValueSize(), result );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ InterpolantFactoryMethodLinear: function( result ) {
|
|
|
+
|
|
|
+ return new THREE.LinearInterpolant(
|
|
|
+ this.times, this.values, this.getValueSize(), result );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ InterpolantFactoryMethodSmooth: function( result ) {
|
|
|
+
|
|
|
+ return new THREE.SmoothInterpolant(
|
|
|
+ this.times, this.values, this.getValueSize(), result );
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ setInterpolation: function( interpolation ) {
|
|
|
+
|
|
|
+ var factoryMethod = undefined;
|
|
|
|
|
|
- if( this.lastIndex >= this.keys.length ) {
|
|
|
+ switch ( interpolation ) {
|
|
|
|
|
|
- this.setResult( this.keys[ this.keys.length - 1 ].value );
|
|
|
+ case THREE.InterpolateDiscrete:
|
|
|
|
|
|
- return this.result;
|
|
|
+ factoryMethod = this.InterpolantFactoryMethodDiscrete;
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ case THREE.InterpolateLinear:
|
|
|
+
|
|
|
+ factoryMethod = this.InterpolantFactoryMethodLinear;
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ case THREE.InterpolateSmooth:
|
|
|
+
|
|
|
+ factoryMethod = this.InterpolantFactoryMethodSmooth;
|
|
|
+
|
|
|
+ break;
|
|
|
|
|
|
}
|
|
|
|
|
|
- if( this.lastIndex === 0 ) {
|
|
|
+ if ( factoryMethod === undefined ) {
|
|
|
|
|
|
- this.setResult( this.keys[ 0 ].value );
|
|
|
+ var message = "unsupported interpolation for " +
|
|
|
+ this.ValueTypeName + " keyframe track named " + this.name;
|
|
|
|
|
|
- return this.result;
|
|
|
+ if ( this.createInterpolant === undefined ) {
|
|
|
+
|
|
|
+ // fall back to default, unless the default itself is messed up
|
|
|
+ if ( interpolation !== this.DefaultInterpolation ) {
|
|
|
+
|
|
|
+ this.setInterpolation( this.DefaultInterpolation );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ throw new Error( message ); // fatal, in this case
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ console.warn( message );
|
|
|
+ return;
|
|
|
|
|
|
}
|
|
|
|
|
|
- var prevKey = this.keys[ this.lastIndex - 1 ];
|
|
|
- this.setResult( prevKey.value );
|
|
|
+ this.createInterpolant = factoryMethod;
|
|
|
|
|
|
- // if true, means that prev/current keys are identical, thus no interpolation required.
|
|
|
- if( prevKey.constantToNext ) {
|
|
|
+ },
|
|
|
|
|
|
- return this.result;
|
|
|
+ getInterpolation: function() {
|
|
|
+
|
|
|
+ switch ( this.createInterpolant ) {
|
|
|
+
|
|
|
+ case this.InterpolantFactoryMethodDiscrete:
|
|
|
+
|
|
|
+ return THREE.InterpolateDiscrete;
|
|
|
+
|
|
|
+ case this.InterpolantFactoryMethodLinear:
|
|
|
+
|
|
|
+ return THREE.InterpolateLinear;
|
|
|
+
|
|
|
+ case this.InterpolantFactoryMethodSmooth:
|
|
|
+
|
|
|
+ return THREE.InterpolateSmooth;
|
|
|
|
|
|
}
|
|
|
|
|
|
- // linear interpolation to start with
|
|
|
- var currentKey = this.keys[ this.lastIndex ];
|
|
|
- var alpha = ( time - prevKey.time ) / ( currentKey.time - prevKey.time );
|
|
|
- this.result = this.lerpValues( this.result, currentKey.value, alpha );
|
|
|
+ },
|
|
|
|
|
|
- return this.result;
|
|
|
+ getValueSize: function() {
|
|
|
+
|
|
|
+ return this.values.length / this.times.length;
|
|
|
|
|
|
},
|
|
|
|
|
@@ -78,8 +147,12 @@ THREE.KeyframeTrack.prototype = {
|
|
|
|
|
|
if( timeOffset !== 0.0 ) {
|
|
|
|
|
|
- for( var i = 0; i < this.keys.length; i ++ ) {
|
|
|
- this.keys[i].time += timeOffset;
|
|
|
+ var times = this.times;
|
|
|
+
|
|
|
+ for( var i = 0, n = times.length; i !== n; ++ i ) {
|
|
|
+
|
|
|
+ times[ i ] += timeOffset;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
}
|
|
@@ -93,8 +166,12 @@ THREE.KeyframeTrack.prototype = {
|
|
|
|
|
|
if( timeScale !== 1.0 ) {
|
|
|
|
|
|
- for( var i = 0; i < this.keys.length; i ++ ) {
|
|
|
- this.keys[i].time *= timeScale;
|
|
|
+ var times = this.times;
|
|
|
+
|
|
|
+ for( var i = 0, n = times.length; i !== n; ++ i ) {
|
|
|
+
|
|
|
+ times[ i ] *= timeScale;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
}
|
|
@@ -105,80 +182,109 @@ THREE.KeyframeTrack.prototype = {
|
|
|
|
|
|
// removes keyframes before and after animation without changing any values within the range [startTime, endTime].
|
|
|
// IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
|
|
|
- trim: function( startTime, endTime ) {
|
|
|
+ trim: function( startTime, endTime ) {
|
|
|
+
|
|
|
+ var times = this.times;
|
|
|
+ var nKeys = times.length;
|
|
|
|
|
|
var firstKeysToRemove = 0;
|
|
|
- for( var i = 1; i < this.keys.length; i ++ ) {
|
|
|
- if( this.keys[i] <= startTime ) {
|
|
|
- firstKeysToRemove ++;
|
|
|
- }
|
|
|
+ for ( var i = 1; i !== nKeys; ++ i ) {
|
|
|
+
|
|
|
+ if ( times[i] <= startTime ) ++ firstKeysToRemove;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
var lastKeysToRemove = 0;
|
|
|
- for( var i = this.keys.length - 2; i > 0; i ++ ) {
|
|
|
- if( this.keys[i] >= endTime ) {
|
|
|
- lastKeysToRemove ++;
|
|
|
- }
|
|
|
- else {
|
|
|
- break;
|
|
|
- }
|
|
|
+ for ( var i = nKeys - 2; i !== 0; -- i ) {
|
|
|
+
|
|
|
+ if ( times[i] >= endTime ) ++ lastKeysToRemove;
|
|
|
+ else break;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
// remove last keys first because it doesn't affect the position of the first keys (the otherway around doesn't work as easily)
|
|
|
- if( ( firstKeysToRemove + lastKeysToRemove ) > 0 ) {
|
|
|
- this.keys = this.keys.splice( firstKeysToRemove, this.keys.length - lastKeysToRemove - firstKeysToRemove );;
|
|
|
- }
|
|
|
+ if( ( firstKeysToRemove + lastKeysToRemove ) !== 0 ) {
|
|
|
|
|
|
- return this;
|
|
|
+ var from = firstKeysToRemove;
|
|
|
+ var to = nKeys - lastKeysToRemove - firstKeysToRemove;
|
|
|
|
|
|
- },
|
|
|
+ this.times = THREE.AnimationUtils.arraySlice( times, from, to );
|
|
|
|
|
|
- /* NOTE: This is commented out because we really shouldn't have to handle unsorted key lists
|
|
|
- Tracks with out of order keys should be considered to be invalid. - bhouston
|
|
|
- sort: function() {
|
|
|
+ var values = this.values;
|
|
|
+ var stride = this.getValueSize();
|
|
|
+ this.values = THREE.AnimationUtils.arraySlice( values, from * stride, to * stride );
|
|
|
|
|
|
- this.keys.sort( THREE.KeyframeTrack.keyComparer );
|
|
|
+ }
|
|
|
|
|
|
return this;
|
|
|
|
|
|
- },*/
|
|
|
+ },
|
|
|
|
|
|
// ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
|
|
|
// One could eventually ensure that all key.values in a track are all of the same type (otherwise interpolation makes no sense.)
|
|
|
validate: function() {
|
|
|
|
|
|
- var prevKey = null;
|
|
|
+ if ( this.getValueSize() % 1 !== 0 ) {
|
|
|
+
|
|
|
+ throw new Error( "invalid value size for track named " + this.name );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var times = this.times;
|
|
|
+ var values = this.values;
|
|
|
+ var stride = this.getValueSize();
|
|
|
+
|
|
|
+ var nKeys = times.length;
|
|
|
+
|
|
|
+ if( nKeys === 0 ) {
|
|
|
|
|
|
- if( this.keys.length === 0 ) {
|
|
|
console.error( " track is empty, no keys", this );
|
|
|
return;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
- for( var i = 0; i < this.keys.length; i ++ ) {
|
|
|
+ var prevTime = null;
|
|
|
+
|
|
|
+ for( var i = 0; i !== nKeys; i ++ ) {
|
|
|
+
|
|
|
+ var currTime = times[ i ];
|
|
|
|
|
|
- var currKey = this.keys[i];
|
|
|
+ if( currTime === undefined || currTime === null ) {
|
|
|
|
|
|
- if( ! currKey ) {
|
|
|
- console.error( " key is null in track", this, i );
|
|
|
+ console.error( " time is null in track", this, i );
|
|
|
return;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
- if ( ( typeof currKey.time ) !== 'number' || isNaN( currKey.time ) ) {
|
|
|
- console.error( " key.time is not a valid number", this, i, currKey );
|
|
|
+ if( ( typeof currTime ) !== 'number' || Number.isNaN( currTime ) ) {
|
|
|
+
|
|
|
+ console.error( " time is not a valid number", this, i, currTime );
|
|
|
return;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
- if( currKey.value === undefined || currKey.value === null) {
|
|
|
- console.error( " key.value is null in track", this, i, currKey );
|
|
|
- return;
|
|
|
+ var offset = i * stride;
|
|
|
+ for ( var j = offset, e = offset + stride; j !== e; ++ j ) {
|
|
|
+
|
|
|
+ var value = values[ j ];
|
|
|
+
|
|
|
+ if( value === undefined || value === null) {
|
|
|
+
|
|
|
+ console.error( " value is null in track", this, j, value );
|
|
|
+ return;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
- if( prevKey && prevKey.time > currKey.time ) {
|
|
|
- console.error( " key.time is less than previous key time, out of order keys", this, i, currKey, prevKey );
|
|
|
+ if( prevTime !== null && prevTime > currTime ) {
|
|
|
+
|
|
|
+ console.error( " time is less than previous key time, out of order keys", this, i, currTime, prevTime );
|
|
|
return;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
- prevKey = currKey;
|
|
|
+ prevTime = currTime;
|
|
|
|
|
|
}
|
|
|
|
|
@@ -186,43 +292,80 @@ THREE.KeyframeTrack.prototype = {
|
|
|
|
|
|
},
|
|
|
|
|
|
- // currently only removes equivalent sequential keys (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0), which are common in morph target animations
|
|
|
+ // removes equivalent sequential keys as common in morph target sequences
|
|
|
+ // (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
|
|
|
optimize: function() {
|
|
|
|
|
|
- var newKeys = [];
|
|
|
- var prevKey = this.keys[0];
|
|
|
- newKeys.push( prevKey );
|
|
|
+ var times = this.times;
|
|
|
+ var values = this.values;
|
|
|
+ var stride = this.getValueSize();
|
|
|
|
|
|
- var equalsFunc = THREE.AnimationUtils.getEqualsFunc( prevKey.value );
|
|
|
+ var writeIndex = 1;
|
|
|
|
|
|
- for( var i = 1; i < this.keys.length - 1; i ++ ) {
|
|
|
- var currKey = this.keys[i];
|
|
|
- var nextKey = this.keys[i+1];
|
|
|
+ for( var i = 1, n = times.length - 1; i <= n; ++ i ) {
|
|
|
|
|
|
- // if prevKey & currKey are the same time, remove currKey. If you want immediate adjacent keys, use an epsilon offset
|
|
|
- // it is not possible to have two keys at the same time as we sort them. The sort is not stable on keys with the same time.
|
|
|
- if( ( prevKey.time === currKey.time ) ) {
|
|
|
+ var keep = false;
|
|
|
|
|
|
- continue;
|
|
|
+ var time = times[ i ];
|
|
|
+ var timeNext = times[ i + 1 ];
|
|
|
|
|
|
- }
|
|
|
+ // remove adjacent keyframes scheduled at the same time
|
|
|
+
|
|
|
+ if ( time !== timeNext && ( i !== 1 || time !== time[ 0 ] ) ) {
|
|
|
|
|
|
- // remove completely unnecessary keyframes that are the same as their prev and next keys
|
|
|
- if( this.compareValues( prevKey.value, currKey.value ) && this.compareValues( currKey.value, nextKey.value ) ) {
|
|
|
+ // remove unnecessary keyframes same as their neighbors
|
|
|
+ var offset = i * stride;
|
|
|
+ var offsetP = offset - stride;
|
|
|
+ var offsetN = offset + stride;
|
|
|
|
|
|
- continue;
|
|
|
+ for ( var j = 0; j !== stride; ++ j ) {
|
|
|
+
|
|
|
+ var value = values[ offset + j ];
|
|
|
+
|
|
|
+ if ( value !== values[ offsetP + j ] ||
|
|
|
+ value !== values[ offsetN + j ] ) {
|
|
|
+
|
|
|
+ keep = true;
|
|
|
+ break;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
}
|
|
|
|
|
|
- // determine if interpolation is required
|
|
|
- prevKey.constantToNext = this.compareValues( prevKey.value, currKey.value );
|
|
|
+ // in-place compaction
|
|
|
+
|
|
|
+ if ( keep ) {
|
|
|
+
|
|
|
+ if ( i !== writeIndex ) {
|
|
|
+
|
|
|
+ times[ writeIndex ] = times[ i ];
|
|
|
+
|
|
|
+ var readOffset = i * stride;
|
|
|
+ var writeOffset = writeIndex * stride;
|
|
|
+
|
|
|
+ for ( var j = 0; j !== stride; ++ j ) {
|
|
|
+
|
|
|
+ values[ writeOffset + j ] = values[ readOffset + j ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ ++ writeIndex;
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
- newKeys.push( currKey );
|
|
|
- prevKey = currKey;
|
|
|
}
|
|
|
- newKeys.push( this.keys[ this.keys.length - 1 ] );
|
|
|
|
|
|
- this.keys = newKeys;
|
|
|
+ if ( writeIndex !== times.length ) {
|
|
|
+
|
|
|
+ this.times = THREE.AnimationUtils.arraySlice( times, 0, writeIndex );
|
|
|
+ this.values = THREE.AnimationUtils.arraySlice( values, 0, writeIndex * stride );
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
return this;
|
|
|
|
|
@@ -230,45 +373,126 @@ THREE.KeyframeTrack.prototype = {
|
|
|
|
|
|
};
|
|
|
|
|
|
-THREE.KeyframeTrack.keyComparer = function keyComparator(key0, key1) {
|
|
|
- return key0.time - key1.time;
|
|
|
-};
|
|
|
+// Serialization (in static context, because of constructor invocation
|
|
|
+// and automatic invocation of .toJSON):
|
|
|
|
|
|
THREE.KeyframeTrack.parse = function( json ) {
|
|
|
|
|
|
- if( json.type === undefined ) throw new Error( "track type undefined, can not parse" );
|
|
|
+ if( json[ 'type' ] === undefined ) {
|
|
|
+
|
|
|
+ throw new Error( "track type undefined, can not parse" );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var trackType = THREE.KeyframeTrack.GetTrackTypeForValueTypeName( json.type );
|
|
|
|
|
|
- var trackType = THREE.KeyframeTrack.GetTrackTypeForTypeName( json.type );
|
|
|
+ if ( json[ 'times' ] === undefined ) {
|
|
|
|
|
|
- return trackType.parse( json );
|
|
|
+ console.warn( "legacy JSON format detected, converting" );
|
|
|
+
|
|
|
+ var times = [], values = [];
|
|
|
+
|
|
|
+ THREE.AnimationUtils.flattenJSON( json, times, values, 'value' );
|
|
|
+
|
|
|
+ json[ 'times' ] = times;
|
|
|
+ json[ 'values' ] = values;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // derived classes can define a static parse method
|
|
|
+ if ( trackType.parse !== undefined ) {
|
|
|
+
|
|
|
+ return trackType.parse( json );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // by default, we asssume a constructor compatible with the base
|
|
|
+ return new trackType(
|
|
|
+ json[ 'name' ], json[ 'times' ], json[ 'values' ], json[ 'interpolation' ] );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+THREE.KeyframeTrack.toJSON = function( track ) {
|
|
|
+
|
|
|
+ var trackType = track.constructor;
|
|
|
+
|
|
|
+ var json;
|
|
|
+
|
|
|
+ // derived classes can define a static toJSON method
|
|
|
+ if ( trackType.toJSON !== undefined ) {
|
|
|
+
|
|
|
+ json = trackType.toJSON( track );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // by default, we assume the data can be serialized as-is
|
|
|
+ json = {
|
|
|
+
|
|
|
+ 'name': track.name,
|
|
|
+ 'times': THREE.AnimationUtils.convertArray( track.times, Array ),
|
|
|
+ 'values': THREE.AnimationUtils.convertArray( track.values, Array )
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ var interpolation = track.getInterpolation();
|
|
|
+
|
|
|
+ if ( interpolation !== track.DefaultInterpolation ) {
|
|
|
+
|
|
|
+ json[ 'interpolation' ] = interpolation;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ json[ 'type' ] = track.ValueTypeName; // mandatory
|
|
|
+
|
|
|
+ return json;
|
|
|
|
|
|
};
|
|
|
|
|
|
-THREE.KeyframeTrack.GetTrackTypeForTypeName = function( typeName ) {
|
|
|
+THREE.KeyframeTrack.GetTrackTypeForValueTypeName = function( typeName ) {
|
|
|
+
|
|
|
switch( typeName.toLowerCase() ) {
|
|
|
- case "vector":
|
|
|
- case "vector2":
|
|
|
- case "vector3":
|
|
|
- case "vector4":
|
|
|
+
|
|
|
+ case "scalar":
|
|
|
+ case "double":
|
|
|
+ case "float":
|
|
|
+ case "number":
|
|
|
+ case "integer":
|
|
|
+
|
|
|
+ return THREE.NumberKeyframeTrack;
|
|
|
+
|
|
|
+ case "vector":
|
|
|
+ case "vector2":
|
|
|
+ case "vector3":
|
|
|
+ case "vector4":
|
|
|
+
|
|
|
return THREE.VectorKeyframeTrack;
|
|
|
|
|
|
- case "quaternion":
|
|
|
+ case "color":
|
|
|
+
|
|
|
+ return THREE.ColorKeyframeTrack;
|
|
|
+
|
|
|
+ case "quaternion":
|
|
|
+
|
|
|
return THREE.QuaternionKeyframeTrack;
|
|
|
|
|
|
- case "integer":
|
|
|
- case "scalar":
|
|
|
- case "double":
|
|
|
- case "float":
|
|
|
- case "number":
|
|
|
- return THREE.NumberKeyframeTrack;
|
|
|
+ case "bool":
|
|
|
+ case "boolean":
|
|
|
|
|
|
- case "bool":
|
|
|
- case "boolean":
|
|
|
return THREE.BooleanKeyframeTrack;
|
|
|
|
|
|
- case "string":
|
|
|
- return THREE.StringKeyframeTrack;
|
|
|
+ case "string":
|
|
|
+
|
|
|
+ return THREE.StringKeyframeTrack;
|
|
|
+
|
|
|
};
|
|
|
|
|
|
throw new Error( "Unsupported typeName: " + typeName );
|
|
|
+<<<<<<< HEAD
|
|
|
+=======
|
|
|
+
|
|
|
+>>>>>>> Animation: Interpolants & extensibility overhaul.
|
|
|
};
|