starry / backend /libs /three /animation /KeyframeTrack.js
k-l-lambda's picture
feat: add Python ML services (CPU mode) with model download
2b7aae2
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 };