Spaces:
Sleeping
Sleeping
| import { InterpolateLinear, InterpolateSmooth, InterpolateDiscrete } from '../constants.js'; | |
| import { CubicInterpolant } from '../math/interpolants/CubicInterpolant.js'; | |
| import { LinearInterpolant } from '../math/interpolants/LinearInterpolant.js'; | |
| import { DiscreteInterpolant } from '../math/interpolants/DiscreteInterpolant.js'; | |
| import { AnimationUtils } from './AnimationUtils.js'; | |
| class KeyframeTrack { | |
| constructor(name, times, values, interpolation) { | |
| if (name === undefined) throw new Error('THREE.KeyframeTrack: track name is undefined'); | |
| if (times === undefined || times.length === 0) throw new Error('THREE.KeyframeTrack: no keyframes in track named ' + name); | |
| this.name = name; | |
| this.times = AnimationUtils.convertArray(times, this.TimeBufferType); | |
| this.values = AnimationUtils.convertArray(values, this.ValueBufferType); | |
| this.setInterpolation(interpolation || this.DefaultInterpolation); | |
| } | |
| // Serialization (in static context, because of constructor invocation | |
| // and automatic invocation of .toJSON): | |
| static toJSON(track) { | |
| const trackType = track.constructor; | |
| let json; | |
| // derived classes can define a static toJSON method | |
| if (trackType.toJSON !== this.toJSON) { | |
| json = trackType.toJSON(track); | |
| } else { | |
| // by default, we assume the data can be serialized as-is | |
| json = { | |
| name: track.name, | |
| times: AnimationUtils.convertArray(track.times, Array), | |
| values: AnimationUtils.convertArray(track.values, Array), | |
| }; | |
| const interpolation = track.getInterpolation(); | |
| if (interpolation !== track.DefaultInterpolation) { | |
| json.interpolation = interpolation; | |
| } | |
| } | |
| json.type = track.ValueTypeName; // mandatory | |
| return json; | |
| } | |
| InterpolantFactoryMethodDiscrete(result) { | |
| return new DiscreteInterpolant(this.times, this.values, this.getValueSize(), result); | |
| } | |
| InterpolantFactoryMethodLinear(result) { | |
| return new LinearInterpolant(this.times, this.values, this.getValueSize(), result); | |
| } | |
| InterpolantFactoryMethodSmooth(result) { | |
| return new CubicInterpolant(this.times, this.values, this.getValueSize(), result); | |
| } | |
| setInterpolation(interpolation) { | |
| let factoryMethod; | |
| switch (interpolation) { | |
| case InterpolateDiscrete: | |
| factoryMethod = this.InterpolantFactoryMethodDiscrete; | |
| break; | |
| case InterpolateLinear: | |
| factoryMethod = this.InterpolantFactoryMethodLinear; | |
| break; | |
| case InterpolateSmooth: | |
| factoryMethod = this.InterpolantFactoryMethodSmooth; | |
| break; | |
| } | |
| if (factoryMethod === undefined) { | |
| const message = 'unsupported interpolation for ' + this.ValueTypeName + ' keyframe track named ' + this.name; | |
| 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('THREE.KeyframeTrack:', message); | |
| return this; | |
| } | |
| this.createInterpolant = factoryMethod; | |
| return this; | |
| } | |
| getInterpolation() { | |
| switch (this.createInterpolant) { | |
| case this.InterpolantFactoryMethodDiscrete: | |
| return InterpolateDiscrete; | |
| case this.InterpolantFactoryMethodLinear: | |
| return InterpolateLinear; | |
| case this.InterpolantFactoryMethodSmooth: | |
| return InterpolateSmooth; | |
| } | |
| } | |
| getValueSize() { | |
| return this.values.length / this.times.length; | |
| } | |
| // move all keyframes either forwards or backwards in time | |
| shift(timeOffset) { | |
| if (timeOffset !== 0.0) { | |
| const times = this.times; | |
| for (let i = 0, n = times.length; i !== n; ++i) { | |
| times[i] += timeOffset; | |
| } | |
| } | |
| return this; | |
| } | |
| // scale all keyframe times by a factor (useful for frame <-> seconds conversions) | |
| scale(timeScale) { | |
| if (timeScale !== 1.0) { | |
| const times = this.times; | |
| for (let i = 0, n = times.length; i !== n; ++i) { | |
| times[i] *= timeScale; | |
| } | |
| } | |
| return this; | |
| } | |
| // 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(startTime, endTime) { | |
| const times = this.times, | |
| nKeys = times.length; | |
| let from = 0, | |
| to = nKeys - 1; | |
| while (from !== nKeys && times[from] < startTime) { | |
| ++from; | |
| } | |
| while (to !== -1 && times[to] > endTime) { | |
| --to; | |
| } | |
| ++to; // inclusive -> exclusive bound | |
| if (from !== 0 || to !== nKeys) { | |
| // empty tracks are forbidden, so keep at least one keyframe | |
| if (from >= to) { | |
| to = Math.max(to, 1); | |
| from = to - 1; | |
| } | |
| const stride = this.getValueSize(); | |
| this.times = AnimationUtils.arraySlice(times, from, to); | |
| this.values = AnimationUtils.arraySlice(this.values, from * stride, to * stride); | |
| } | |
| return this; | |
| } | |
| // ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable | |
| validate() { | |
| let valid = true; | |
| const valueSize = this.getValueSize(); | |
| if (valueSize - Math.floor(valueSize) !== 0) { | |
| console.error('THREE.KeyframeTrack: Invalid value size in track.', this); | |
| valid = false; | |
| } | |
| const times = this.times, | |
| values = this.values, | |
| nKeys = times.length; | |
| if (nKeys === 0) { | |
| console.error('THREE.KeyframeTrack: Track is empty.', this); | |
| valid = false; | |
| } | |
| let prevTime = null; | |
| for (let i = 0; i !== nKeys; i++) { | |
| const currTime = times[i]; | |
| if (typeof currTime === 'number' && isNaN(currTime)) { | |
| console.error('THREE.KeyframeTrack: Time is not a valid number.', this, i, currTime); | |
| valid = false; | |
| break; | |
| } | |
| if (prevTime !== null && prevTime > currTime) { | |
| console.error('THREE.KeyframeTrack: Out of order keys.', this, i, currTime, prevTime); | |
| valid = false; | |
| break; | |
| } | |
| prevTime = currTime; | |
| } | |
| if (values !== undefined) { | |
| if (AnimationUtils.isTypedArray(values)) { | |
| for (let i = 0, n = values.length; i !== n; ++i) { | |
| const value = values[i]; | |
| if (isNaN(value)) { | |
| console.error('THREE.KeyframeTrack: Value is not a valid number.', this, i, value); | |
| valid = false; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| return valid; | |
| } | |
| // 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() { | |
| // times or values may be shared with other tracks, so overwriting is unsafe | |
| const times = AnimationUtils.arraySlice(this.times), | |
| values = AnimationUtils.arraySlice(this.values), | |
| stride = this.getValueSize(), | |
| smoothInterpolation = this.getInterpolation() === InterpolateSmooth, | |
| lastIndex = times.length - 1; | |
| let writeIndex = 1; | |
| for (let i = 1; i < lastIndex; ++i) { | |
| let keep = false; | |
| const time = times[i]; | |
| const timeNext = times[i + 1]; | |
| // remove adjacent keyframes scheduled at the same time | |
| if (time !== timeNext && (i !== 1 || time !== times[0])) { | |
| if (!smoothInterpolation) { | |
| // remove unnecessary keyframes same as their neighbors | |
| const offset = i * stride, | |
| offsetP = offset - stride, | |
| offsetN = offset + stride; | |
| for (let j = 0; j !== stride; ++j) { | |
| const value = values[offset + j]; | |
| if (value !== values[offsetP + j] || value !== values[offsetN + j]) { | |
| keep = true; | |
| break; | |
| } | |
| } | |
| } else { | |
| keep = true; | |
| } | |
| } | |
| // in-place compaction | |
| if (keep) { | |
| if (i !== writeIndex) { | |
| times[writeIndex] = times[i]; | |
| const readOffset = i * stride, | |
| writeOffset = writeIndex * stride; | |
| for (let j = 0; j !== stride; ++j) { | |
| values[writeOffset + j] = values[readOffset + j]; | |
| } | |
| } | |
| ++writeIndex; | |
| } | |
| } | |
| // flush last keyframe (compaction looks ahead) | |
| if (lastIndex > 0) { | |
| times[writeIndex] = times[lastIndex]; | |
| for (let readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j !== stride; ++j) { | |
| values[writeOffset + j] = values[readOffset + j]; | |
| } | |
| ++writeIndex; | |
| } | |
| if (writeIndex !== times.length) { | |
| this.times = AnimationUtils.arraySlice(times, 0, writeIndex); | |
| this.values = AnimationUtils.arraySlice(values, 0, writeIndex * stride); | |
| } else { | |
| this.times = times; | |
| this.values = values; | |
| } | |
| return this; | |
| } | |
| clone() { | |
| const times = AnimationUtils.arraySlice(this.times, 0); | |
| const values = AnimationUtils.arraySlice(this.values, 0); | |
| const TypedKeyframeTrack = this.constructor; | |
| const track = new TypedKeyframeTrack(this.name, times, values); | |
| // Interpolant argument to constructor is not saved, so copy the factory method directly. | |
| track.createInterpolant = this.createInterpolant; | |
| return track; | |
| } | |
| } | |
| KeyframeTrack.prototype.TimeBufferType = Float32Array; | |
| KeyframeTrack.prototype.ValueBufferType = Float32Array; | |
| KeyframeTrack.prototype.DefaultInterpolation = InterpolateLinear; | |
| export { KeyframeTrack }; | |