mirror of
https://github.com/JannisX11/blockbench.git
synced 2024-12-03 04:40:46 +08:00
28cbf8f8f6
Add setting to only show handles of selected bezier keyframes
791 lines
22 KiB
JavaScript
791 lines
22 KiB
JavaScript
class GeneralAnimator {
|
|
constructor(uuid, animation) {
|
|
this.animation = animation;
|
|
this.expanded = false;
|
|
this.selected = false;
|
|
this.uuid = uuid || guid();
|
|
this.muted = {};
|
|
for (let channel in this.channels) {
|
|
this.muted[channel] = false;
|
|
}
|
|
}
|
|
get keyframes() {
|
|
let array = [];
|
|
for (let channel in this.channels) {
|
|
if (this[channel] && this[channel].length) array.push(...this[channel]);
|
|
}
|
|
return array;
|
|
}
|
|
select() {
|
|
var scope = this;
|
|
for (var key in this.animation.animators) {
|
|
this.animation.animators[key].selected = false;
|
|
}
|
|
this.selected = true;
|
|
Timeline.selected_animator = this;
|
|
this.addToTimeline();
|
|
Vue.nextTick(() => {
|
|
scope.scrollTo();
|
|
})
|
|
return this;
|
|
}
|
|
addToTimeline() {
|
|
if (!Timeline.animators.includes(this)) {
|
|
Timeline.animators.splice(0, 0, this);
|
|
}
|
|
for (let channel in this.channels) {
|
|
if (!this[channel]) this[channel] = [];
|
|
}
|
|
if (!this.expanded) this.expanded = true;
|
|
return this;
|
|
}
|
|
addKeyframe(data, uuid) {
|
|
var channel = data.channel;
|
|
if (typeof channel == 'number') channel = Object.keys(this.channels)[channel];
|
|
if (channel && this[channel]) {
|
|
var kf = new Keyframe(data, uuid, this);
|
|
this[channel].push(kf);
|
|
kf.animator = this;
|
|
return kf;
|
|
}
|
|
}
|
|
createKeyframe(value, time, channel, undo, select) {
|
|
if (!this.channels[channel]) return;
|
|
if (typeof time !== 'number') time = Timeline.time;
|
|
var keyframes = [];
|
|
if (undo) {
|
|
Undo.initEdit({keyframes})
|
|
}
|
|
var keyframe = new Keyframe({
|
|
channel: channel,
|
|
time: time
|
|
}, null, this);
|
|
keyframes.push(keyframe);
|
|
|
|
if (value) {
|
|
keyframe.extend(value);
|
|
} else if (this.channels[channel].transform && this.fillValues) {
|
|
this.fillValues(keyframe, value, true);
|
|
}
|
|
|
|
keyframe.channel = channel;
|
|
keyframe.time = Timeline.snapTime(time);
|
|
|
|
this[channel].push(keyframe);
|
|
keyframe.animator = this;
|
|
|
|
if (select !== false) {
|
|
keyframe.select();
|
|
}
|
|
var deleted = [];
|
|
delete keyframe.time_before;
|
|
keyframe.replaceOthers(deleted);
|
|
Undo.addKeyframeCasualties(deleted);
|
|
Animation.selected.setLength();
|
|
|
|
if (undo) {
|
|
Undo.finishEdit('Add keyframe')
|
|
}
|
|
return keyframe;
|
|
}
|
|
getOrMakeKeyframe(channel) {
|
|
let before, result;
|
|
let epsilon = Timeline.getStep()/2 || 0.01;
|
|
|
|
for (let kf of this[channel]) {
|
|
if (Math.abs(kf.time - Timeline.time) <= epsilon) {
|
|
before = kf;
|
|
}
|
|
}
|
|
result = before ? before : this.createKeyframe(null, Timeline.time, channel, false, false);
|
|
return {before, result};
|
|
}
|
|
showContextMenu(event) {
|
|
Prop.active_panel = 'timeline'
|
|
if (!this.selected) {
|
|
this.select()
|
|
}
|
|
if (this.menu) {
|
|
this.menu.open(event, this);
|
|
}
|
|
return this;
|
|
}
|
|
toggleMuted(channel) {
|
|
this.muted[channel] = !this.muted[channel];
|
|
if (this instanceof BoneAnimator) Animator.preview();
|
|
return this;
|
|
}
|
|
scrollTo() {
|
|
var el = $(`#timeline_body_inner > li[uuid=${this.uuid}]`).get(0)
|
|
if (el) {
|
|
var offset = el.offsetTop;
|
|
var timeline = document.getElementById('timeline_body');
|
|
var scroll_top = timeline.scrollTop;
|
|
var height = timeline.clientHeight;
|
|
if (offset < scroll_top) {
|
|
$(timeline).animate({
|
|
scrollTop: offset
|
|
}, 200);
|
|
}
|
|
if (offset + el.clientHeight > scroll_top + height) {
|
|
$(timeline).animate({
|
|
scrollTop: offset - (height-el.clientHeight-20)
|
|
}, 200);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GeneralAnimator.addChannel = function(channel, options) {
|
|
this.prototype.channels[channel] = {
|
|
name: options.name || channel,
|
|
transform: options.transform || false,
|
|
mutable: typeof options.mutable === 'boolean' ? options.mutable : true,
|
|
max_data_points: options.max_data_points || 0
|
|
}
|
|
ModelProject.all.forEach(project => {
|
|
if (!project.animations)
|
|
project.animations.forEach(animation => {
|
|
animation.animators.forEach(animator => {
|
|
if (animator instanceof this && !animator[channel]) {
|
|
Vue.set(animator, channel, []);
|
|
if (this.prototype.channels[channel].mutable) {
|
|
Vue.set(animator.muted, channel, false);
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
Timeline.vue.$forceUpdate();
|
|
}
|
|
class BoneAnimator extends GeneralAnimator {
|
|
constructor(uuid, animation, name) {
|
|
super(uuid, animation);
|
|
this.uuid = uuid;
|
|
this._name = name;
|
|
this.rotation_global = false;
|
|
|
|
for (let channel in this.channels) {
|
|
this[channel] = [];
|
|
}
|
|
}
|
|
get name() {
|
|
var group = this.getGroup();
|
|
if (group) return group.name;
|
|
return this._name;
|
|
}
|
|
set name(name) {
|
|
this._name = name;
|
|
}
|
|
getGroup() {
|
|
this.group = OutlinerNode.uuids[this.uuid];
|
|
return this.group
|
|
}
|
|
select(group_is_selected) {
|
|
if (!this.getGroup()) {
|
|
unselectAll();
|
|
return this;
|
|
}
|
|
if (this.group.locked) return;
|
|
|
|
var duplicates;
|
|
for (var key in this.animation.animators) {
|
|
this.animation.animators[key].selected = false;
|
|
}
|
|
if (group_is_selected !== true && this.group) {
|
|
this.group.select();
|
|
}
|
|
Group.all.forEach(group => {
|
|
if (group.name == group.selected.name && group != Group.selected) {
|
|
duplicates = true;
|
|
}
|
|
})
|
|
function iterate(arr) {
|
|
arr.forEach((it) => {
|
|
if (it.type === 'group' && !duplicates) {
|
|
if (it.name === Group.selected.name && it !== Group.selected) {
|
|
duplicates = true;
|
|
} else if (it.children && it.children.length) {
|
|
iterate(it.children);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
iterate(Outliner.root);
|
|
if (duplicates) {
|
|
Blockbench.showMessageBox({
|
|
translateKey: 'duplicate_groups',
|
|
icon: 'folder',
|
|
});
|
|
}
|
|
super.select();
|
|
|
|
if (this[Toolbox.selected.animation_channel] && (Timeline.selected.length == 0 || Timeline.selected[0].animator != this)) {
|
|
var nearest;
|
|
this[Toolbox.selected.animation_channel].forEach(kf => {
|
|
if (Math.abs(kf.time - Timeline.time) < 0.002) {
|
|
nearest = kf;
|
|
}
|
|
})
|
|
if (nearest) {
|
|
nearest.select();
|
|
}
|
|
}
|
|
|
|
if (this.group && this.group.parent && this.group.parent !== 'root') {
|
|
this.group.parent.openUp();
|
|
}
|
|
return this;
|
|
}
|
|
fillValues(keyframe, values, allow_expression, round = true) {
|
|
if (values instanceof Array) {
|
|
keyframe.extend({
|
|
data_points: [{
|
|
x: values[0],
|
|
y: values[1],
|
|
z: values[2]
|
|
}]
|
|
})
|
|
} else if (typeof values === 'number' || typeof values === 'string') {
|
|
keyframe.extend({
|
|
data_points: [{
|
|
x: values,
|
|
y: values,
|
|
z: values
|
|
}]
|
|
})
|
|
} else if (values === null) {
|
|
let closest;
|
|
this[keyframe.channel].forEach(kf => {
|
|
if (!closest || Math.abs(kf.time - keyframe.time) < Math.abs(closest.time - keyframe.time)) {
|
|
closest = kf;
|
|
}
|
|
});
|
|
let interpolation = closest?.interpolation;
|
|
let original_time = Timeline.time;
|
|
Timeline.time = keyframe.time;
|
|
let ref = this.interpolate(keyframe.channel, allow_expression);
|
|
let ref2;
|
|
if (interpolation == 'bezier') {
|
|
Timeline.time = keyframe.time + 0.01;
|
|
ref2 = this.interpolate(keyframe.channel, allow_expression);
|
|
}
|
|
Timeline.time = original_time;
|
|
if (ref) {
|
|
if (round) {
|
|
let e = keyframe.channel == 'scale' ? 1e4 : 1e2
|
|
ref.forEach((r, a) => {
|
|
if (!isNaN(r)) {
|
|
ref[a] = Math.round(parseFloat(r)*e)/e
|
|
}
|
|
})
|
|
}
|
|
keyframe.extend({
|
|
data_points: [{
|
|
x: ref[0],
|
|
y: ref[1],
|
|
z: ref[2],
|
|
}]
|
|
})
|
|
if (interpolation == 'bezier' && ref2) {
|
|
ref.forEach((val1, a) => {
|
|
if (val1 !== ref2[a]) {
|
|
keyframe.bezier_right_value[a] = (ref2[a] - val1) * 10;
|
|
keyframe.bezier_left_value[a] = -keyframe.bezier_right_value[a];
|
|
}
|
|
})
|
|
}
|
|
}
|
|
keyframe.extend({
|
|
interpolation,
|
|
uniform: (keyframe.channel == 'scale')
|
|
? (closest && closest.uniform && closest.data_points[0].x == closest.data_points[0].y && closest.data_points[0].x == closest.data_points[0].z)
|
|
: undefined,
|
|
})
|
|
} else {
|
|
keyframe.extend(values)
|
|
}
|
|
}
|
|
pushKeyframe(keyframe) {
|
|
this[keyframe.channel].push(keyframe)
|
|
keyframe.animator = this;
|
|
return this;
|
|
}
|
|
doRender() {
|
|
this.getGroup()
|
|
if (this.group && this.group.children && this.group.mesh) {
|
|
let mesh = this.group.mesh
|
|
return (mesh && mesh.fix_rotation)
|
|
}
|
|
}
|
|
displayRotation(arr, multiplier = 1) {
|
|
var bone = this.group.mesh
|
|
|
|
if (arr) {
|
|
if (arr.length === 4) {
|
|
var added_rotation = new THREE.Euler().setFromQuaternion(new THREE.Quaternion().fromArray(arr), 'ZYX')
|
|
bone.rotation.x -= added_rotation.x * multiplier
|
|
bone.rotation.y -= added_rotation.y * multiplier
|
|
bone.rotation.z += added_rotation.z * multiplier
|
|
} else {
|
|
arr.forEach((n, i) => {
|
|
bone.rotation[getAxisLetter(i)] += Math.degToRad(n) * (i == 2 ? 1 : -1) * multiplier
|
|
})
|
|
}
|
|
}
|
|
if (this.rotation_global) {
|
|
let quat = bone.parent.getWorldQuaternion(Reusable.quat1);
|
|
quat.invert();
|
|
bone.quaternion.premultiply(quat);
|
|
|
|
}
|
|
return this;
|
|
}
|
|
displayPosition(arr, multiplier = 1) {
|
|
var bone = this.group.mesh
|
|
if (arr) {
|
|
bone.position.x -= arr[0] * multiplier;
|
|
bone.position.y += arr[1] * multiplier;
|
|
bone.position.z += arr[2] * multiplier;
|
|
}
|
|
return this;
|
|
}
|
|
displayScale(arr, multiplier = 1) {
|
|
if (!arr) return this;
|
|
var bone = this.group.mesh;
|
|
bone.scale.x *= (1 + (arr[0] - 1) * multiplier) || 0.00001;
|
|
bone.scale.y *= (1 + (arr[1] - 1) * multiplier) || 0.00001;
|
|
bone.scale.z *= (1 + (arr[2] - 1) * multiplier) || 0.00001;
|
|
return this;
|
|
}
|
|
interpolate(channel, allow_expression, axis) {
|
|
let time = this.animation.time;
|
|
var before = false
|
|
var after = false
|
|
var result = false
|
|
let epsilon = 1/1200;
|
|
|
|
function mapAxes(cb) {
|
|
if (!Animator._last_values[channel]) Animator._last_values[channel] = [0, 0, 0];
|
|
if (axis) {
|
|
let result = cb(axis);
|
|
Animator._last_values[channel][axis] = result;
|
|
return result;
|
|
} else {
|
|
return ['x', 'y', 'z'].map(axis => {
|
|
let result = cb(axis);
|
|
Animator._last_values[channel][axis] = result;
|
|
return result;
|
|
});
|
|
}
|
|
}
|
|
|
|
for (var keyframe of this[channel]) {
|
|
|
|
if (keyframe.time < time) {
|
|
if (!before || keyframe.time > before.time) {
|
|
before = keyframe
|
|
}
|
|
} else {
|
|
if (!after || keyframe.time < after.time) {
|
|
after = keyframe
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
if (before && Math.epsilon(before.time, time, epsilon)) {
|
|
result = before
|
|
} else if (after && Math.epsilon(after.time, time, epsilon)) {
|
|
result = after
|
|
} else if (before && before.interpolation == Keyframe.interpolation.step) {
|
|
result = before
|
|
} else if (before && !after) {
|
|
result = before
|
|
} else if (after && !before) {
|
|
result = after
|
|
} else if (!before && !after) {
|
|
//
|
|
} else {
|
|
let no_interpolations = Blockbench.hasFlag('no_interpolations')
|
|
let alpha = Math.getLerp(before.time, after.time, time)
|
|
|
|
|
|
if (no_interpolations || (before.interpolation === Keyframe.interpolation.linear && after.interpolation === Keyframe.interpolation.linear)) {
|
|
if (no_interpolations) {
|
|
alpha = Math.round(alpha)
|
|
}
|
|
return mapAxes(axis => before.getLerp(after, axis, alpha, allow_expression));
|
|
|
|
} else if (before.interpolation === Keyframe.interpolation.catmullrom || after.interpolation === Keyframe.interpolation.catmullrom) {
|
|
|
|
let sorted = this[channel].slice().sort((kf1, kf2) => (kf1.time - kf2.time));
|
|
let before_index = sorted.indexOf(before);
|
|
let before_plus = sorted[before_index-1];
|
|
let after_plus = sorted[before_index+2];
|
|
|
|
return mapAxes(axis => before.getCatmullromLerp(before_plus, before, after, after_plus, axis, alpha));
|
|
|
|
} else if (before.interpolation === Keyframe.interpolation.bezier || after.interpolation === Keyframe.interpolation.bezier) {
|
|
// Bezier
|
|
return mapAxes(axis => before.getBezierLerp(before, after, axis, alpha));
|
|
}
|
|
}
|
|
if (result && result instanceof Keyframe) {
|
|
let keyframe = result
|
|
let method = allow_expression ? 'get' : 'calc'
|
|
let dp_index = (keyframe.time > time || Math.epsilon(keyframe.time, time, epsilon)) ? 0 : keyframe.data_points.length-1;
|
|
|
|
return mapAxes(axis => keyframe[method](axis, dp_index));
|
|
}
|
|
return false;
|
|
}
|
|
displayFrame(multiplier = 1) {
|
|
if (!this.doRender()) return;
|
|
this.getGroup()
|
|
|
|
if (!this.muted.rotation) this.displayRotation(this.interpolate('rotation'), multiplier)
|
|
if (!this.muted.position) this.displayPosition(this.interpolate('position'), multiplier)
|
|
if (!this.muted.scale) this.displayScale(this.interpolate('scale'), multiplier)
|
|
}
|
|
}
|
|
BoneAnimator.prototype.type = 'bone';
|
|
BoneAnimator.prototype.channels = {
|
|
rotation: {name: tl('timeline.rotation'), mutable: true, transform: true, max_data_points: 2},
|
|
position: {name: tl('timeline.position'), mutable: true, transform: true, max_data_points: 2},
|
|
scale: {name: tl('timeline.scale'), mutable: true, transform: true, max_data_points: 2},
|
|
}
|
|
Group.animator = BoneAnimator;
|
|
BoneAnimator.prototype.menu = new Menu('bone_animator', [
|
|
{
|
|
id: 'rotation_global',
|
|
name: 'menu.animator.rotation_global',
|
|
condition: animator => animator.type == 'bone',
|
|
icon: (animator) => animator.rotation_global,
|
|
click(animator) {
|
|
Undo.initEdit({animations: [Animation.selected]});
|
|
animator.rotation_global = !animator.rotation_global;
|
|
Undo.finishEdit('Toggle rotation in global space');
|
|
Animator.preview();
|
|
}
|
|
}
|
|
])
|
|
|
|
class NullObjectAnimator extends BoneAnimator {
|
|
constructor(uuid, animation, name) {
|
|
super(uuid, animation);
|
|
this.uuid = uuid;
|
|
this._name = name;
|
|
|
|
this.solver = new FIK.Structure3D(scene);
|
|
this.chain = new FIK.Chain3D();
|
|
|
|
this.position = [];
|
|
}
|
|
get name() {
|
|
var element = this.getElement();
|
|
if (element) return element.name;
|
|
return this._name;
|
|
}
|
|
set name(name) {
|
|
this._name = name;
|
|
}
|
|
getElement() {
|
|
this.element = OutlinerNode.uuids[this.uuid];
|
|
return this.element
|
|
}
|
|
select(element_is_selected) {
|
|
if (!this.getElement()) {
|
|
unselectAll();
|
|
return this;
|
|
}
|
|
if (this.getElement().locked) return;
|
|
|
|
if (element_is_selected !== true && this.element) {
|
|
this.element.select();
|
|
}
|
|
GeneralAnimator.prototype.select.call(this);
|
|
|
|
if (this[Toolbox.selected.animation_channel] && (Timeline.selected.length == 0 || Timeline.selected[0].animator != this)) {
|
|
var nearest;
|
|
this[Toolbox.selected.animation_channel].forEach(kf => {
|
|
if (Math.abs(kf.time - Timeline.time) < 0.002) {
|
|
nearest = kf;
|
|
}
|
|
})
|
|
if (nearest) {
|
|
nearest.select();
|
|
}
|
|
}
|
|
|
|
if (this.element && this.element.parent && this.element.parent !== 'root') {
|
|
this.element.parent.openUp();
|
|
}
|
|
return this;
|
|
}
|
|
doRender() {
|
|
this.getElement()
|
|
return (this.element && this.element && this.element.mesh);
|
|
}
|
|
displayPosition(arr, multiplier = 1) {
|
|
var bone = this.element.mesh
|
|
if (arr) {
|
|
bone.position.x -= arr[0] * multiplier;
|
|
bone.position.y += arr[1] * multiplier;
|
|
bone.position.z += arr[2] * multiplier;
|
|
}
|
|
return this;
|
|
}
|
|
displayIK(get_samples) {
|
|
let null_object = this.getElement();
|
|
let target = [...Group.all, ...Locator.all].find(node => node.uuid == null_object.ik_target);
|
|
if (!null_object || !target) return;
|
|
|
|
let bones = [];
|
|
let ik_target = new THREE.Vector3().copy(null_object.getWorldCenter(true));
|
|
let bone_references = [];
|
|
let current = target.parent;
|
|
|
|
let target_original_quaternion = null_object.lock_ik_target_rotation &&
|
|
target instanceof Group &&
|
|
target.mesh.getWorldQuaternion(new THREE.Quaternion());
|
|
|
|
while (current !== null_object.parent) {
|
|
bones.push(current);
|
|
current = current.parent;
|
|
}
|
|
if (!bones.length) return;
|
|
bones.reverse();
|
|
|
|
bones.forEach(bone => {
|
|
if (bone.mesh.fix_rotation) bone.mesh.rotation.copy(bone.mesh.fix_rotation);
|
|
})
|
|
|
|
bones.forEach((bone, i) => {
|
|
let startPoint = new FIK.V3(0,0,0).copy(bone.mesh.getWorldPosition(new THREE.Vector3()));
|
|
let endPoint = new FIK.V3(0,0,0).copy(bones[i+1] ? bones[i+1].mesh.getWorldPosition(new THREE.Vector3()) : null_object.getWorldCenter(false));
|
|
|
|
let ik_bone = new FIK.Bone3D(startPoint, endPoint);
|
|
this.chain.addBone(ik_bone);
|
|
|
|
bone_references.push({
|
|
bone,
|
|
last_diff: new THREE.Vector3(
|
|
(bones[i+1] ? bones[i+1] : target).origin[0] - bone.origin[0],
|
|
(bones[i+1] ? bones[i+1] : target).origin[1] - bone.origin[1],
|
|
(bones[i+1] ? bones[i+1] : target).origin[2] - bone.origin[2]
|
|
).normalize()
|
|
})
|
|
})
|
|
|
|
this.solver.add(this.chain, ik_target , true);
|
|
this.solver.meshChains[0].forEach(mesh => {
|
|
mesh.visible = false;
|
|
})
|
|
|
|
this.solver.update();
|
|
|
|
let results = {};
|
|
bone_references.forEach((bone_ref, i) => {
|
|
let start = Reusable.vec1.copy(this.solver.chains[0].bones[i].start);
|
|
let end = Reusable.vec2.copy(this.solver.chains[0].bones[i].end);
|
|
bones[i].mesh.worldToLocal(start);
|
|
bones[i].mesh.worldToLocal(end);
|
|
|
|
Reusable.quat1.setFromUnitVectors(bone_ref.last_diff, end.sub(start).normalize());
|
|
let rotation = get_samples ? new THREE.Euler() : Reusable.euler1;
|
|
rotation.setFromQuaternion(Reusable.quat1, 'ZYX');
|
|
|
|
bone_ref.bone.mesh.rotation.x += rotation.x;
|
|
bone_ref.bone.mesh.rotation.y += rotation.y;
|
|
bone_ref.bone.mesh.rotation.z += rotation.z;
|
|
bone_ref.bone.mesh.updateMatrixWorld();
|
|
|
|
if (get_samples) {
|
|
results[bone_ref.bone.uuid] = {
|
|
euler: rotation,
|
|
array: [
|
|
Math.radToDeg(-rotation.x),
|
|
Math.radToDeg(-rotation.y),
|
|
Math.radToDeg(rotation.z),
|
|
]
|
|
}
|
|
}
|
|
})
|
|
|
|
if (target_original_quaternion) {
|
|
let rotation = get_samples ? new THREE.Euler() : Reusable.euler1;
|
|
rotation.copy(target.mesh.rotation);
|
|
|
|
target.mesh.quaternion.copy(target_original_quaternion);
|
|
let q1 = target.mesh.parent.getWorldQuaternion(Reusable.quat1);
|
|
target.mesh.quaternion.premultiply(q1.invert())
|
|
target.mesh.updateMatrixWorld();
|
|
|
|
rotation.x = target.mesh.rotation.x - rotation.x;
|
|
rotation.y = target.mesh.rotation.y - rotation.y;
|
|
rotation.z = target.mesh.rotation.z - rotation.z;
|
|
|
|
if (get_samples) {
|
|
results[target.uuid] = {
|
|
euler: rotation,
|
|
array: [
|
|
Math.radToDeg(-rotation.x),
|
|
Math.radToDeg(-rotation.y),
|
|
Math.radToDeg(rotation.z),
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
this.solver.clear();
|
|
this.chain.clear();
|
|
this.chain.lastTargetLocation.set(1e9, 0, 0);
|
|
|
|
if (get_samples) return results;
|
|
}
|
|
displayFrame(multiplier = 1) {
|
|
if (!this.doRender()) return;
|
|
this.getElement()
|
|
|
|
if (!this.muted.position) {
|
|
this.displayPosition(this.interpolate('position'), multiplier);
|
|
this.displayIK();
|
|
}
|
|
}
|
|
}
|
|
NullObjectAnimator.prototype.type = 'null_object';
|
|
NullObjectAnimator.prototype.channels = {
|
|
position: {name: tl('timeline.position'), mutable: true, transform: true, max_data_points: 2},
|
|
}
|
|
NullObject.animator = NullObjectAnimator;
|
|
|
|
class EffectAnimator extends GeneralAnimator {
|
|
constructor(animation) {
|
|
super(null, animation);
|
|
this.last_displayed_time = 0;
|
|
|
|
this.name = tl('timeline.effects')
|
|
this.selected = false;
|
|
|
|
for (let channel in this.channels) {
|
|
this[channel] = [];
|
|
}
|
|
}
|
|
pushKeyframe(keyframe) {
|
|
this[keyframe.channel].push(keyframe)
|
|
keyframe.animator = this;
|
|
return this;
|
|
}
|
|
displayFrame(in_loop) {
|
|
if (in_loop && !this.muted.sound) {
|
|
this.sound.forEach(kf => {
|
|
var diff = kf.time - this.animation.time;
|
|
if (diff >= 0 && diff < (1/60) * (Timeline.playback_speed/100)) {
|
|
if (kf.data_points[0].file && !kf.cooldown) {
|
|
var media = new Audio(kf.data_points[0].file);
|
|
media.playbackRate = Math.clamp(Timeline.playback_speed/100, 0.1, 4.0);
|
|
media.volume = Math.clamp(settings.volume.value/100, 0, 1);
|
|
media.play().catch(() => {});
|
|
Timeline.playing_sounds.push(media);
|
|
media.onended = function() {
|
|
Timeline.playing_sounds.remove(media);
|
|
}
|
|
|
|
kf.cooldown = true;
|
|
setTimeout(() => {
|
|
delete kf.cooldown;
|
|
}, 400)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if (!this.muted.particle) {
|
|
this.particle.forEach(kf => {
|
|
let diff = this.animation.time - kf.time;
|
|
if (diff >= 0) {
|
|
let i = 0;
|
|
for (let data_point of kf.data_points) {
|
|
let particle_effect = data_point.file && Animator.particle_effects[data_point.file]
|
|
if (particle_effect) {
|
|
|
|
let emitter = particle_effect.emitters[kf.uuid + i];
|
|
if (!emitter) {
|
|
let i_here = i;
|
|
let anim_uuid = this.animation.uuid;
|
|
emitter = particle_effect.emitters[kf.uuid + i] = new Wintersky.Emitter(WinterskyScene, particle_effect.config);
|
|
|
|
let old_variable_handler = emitter.Molang.variableHandler;
|
|
emitter.Molang.variableHandler = (key, params) => {
|
|
let curve_result = old_variable_handler.call(emitter, key, params);
|
|
if (curve_result !== undefined) return curve_result;
|
|
return Animator.MolangParser.variableHandler(key);
|
|
}
|
|
emitter.on('start', ({params}) => {
|
|
let animation = Animation.all.find(a => a.uuid === anim_uuid);
|
|
let kf_now = animation?.animators.effects?.particle.find(kf2 => kf2.uuid == kf.uuid);
|
|
let data_point_now = kf_now && kf_now.data_points[i_here];
|
|
if (data_point_now) {
|
|
emitter.Molang.parse(data_point_now.script, Animator.MolangParser.global_variables);
|
|
}
|
|
})
|
|
}
|
|
|
|
let locator = data_point.locator && Locator.all.find(l => l.name == data_point.locator)
|
|
if (locator) {
|
|
locator.mesh.add(emitter.local_space);
|
|
emitter.parent_mode = 'locator';
|
|
} else {
|
|
emitter.parent_mode = 'entity';
|
|
}
|
|
scene.add(emitter.global_space);
|
|
emitter.jumpTo(diff);
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if (!this.muted.timeline) {
|
|
this.timeline.forEach(kf => {
|
|
if ((kf.time > this.last_displayed_time && kf.time <= this.animation.time) || Math.epsilon(kf.time, this.animation.time, 0.01)) {
|
|
let script = kf.data_points[0].script;
|
|
Animator.MolangParser.parse(script);
|
|
}
|
|
})
|
|
}
|
|
|
|
this.last_displayed_time = this.animation.time;
|
|
}
|
|
startPreviousSounds() {
|
|
if (!this.muted.sound) {
|
|
this.sound.forEach(kf => {
|
|
if (kf.data_points[0].file && !kf.cooldown) {
|
|
var diff = kf.time - this.animation.time;
|
|
if (diff < 0 && Timeline.waveforms[kf.data_points[0].file] && Timeline.waveforms[kf.data_points[0].file].duration > -diff) {
|
|
var media = new Audio(kf.data_points[0].file);
|
|
media.playbackRate = Math.clamp(Timeline.playback_speed/100, 0.1, 4.0);
|
|
media.volume = Math.clamp(settings.volume.value/100, 0, 1);
|
|
media.currentTime = -diff;
|
|
media.play().catch(() => {});
|
|
Timeline.playing_sounds.push(media);
|
|
media.onended = function() {
|
|
Timeline.playing_sounds.remove(media);
|
|
}
|
|
|
|
kf.cooldown = true;
|
|
setTimeout(() => {
|
|
delete kf.cooldown;
|
|
}, 400)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
EffectAnimator.prototype.type = 'effect';
|
|
EffectAnimator.prototype.channels = {
|
|
particle: {name: tl('timeline.particle'), mutable: true, max_data_points: 1000},
|
|
sound: {name: tl('timeline.sound'), mutable: true, max_data_points: 1000},
|
|
timeline: {name: tl('timeline.timeline'), mutable: true, max_data_points: 1},
|
|
} |