mirror of
https://github.com/JannisX11/blockbench.git
synced 2024-11-21 01:13:37 +08:00
2558 lines
71 KiB
JavaScript
2558 lines
71 KiB
JavaScript
class Animation {
|
|
constructor(data) {
|
|
this.name = '';
|
|
this.uuid = guid()
|
|
this.loop = 'once';
|
|
this.playing = false;
|
|
this.override = false;
|
|
this.selected = false;
|
|
this.anim_time_update = '';
|
|
this.length = 0;
|
|
this.animators = {};
|
|
this.markers = [];
|
|
if (typeof data === 'object') {
|
|
this.extend(data)
|
|
}
|
|
}
|
|
extend(data) {
|
|
Merge.string(this, data, 'name')
|
|
Merge.string(this, data, 'loop', val => ['once', 'loop', 'hold'].includes(val))
|
|
Merge.boolean(this, data, 'override')
|
|
Merge.string(this, data, 'anim_time_update')
|
|
Merge.number(this, data, 'length')
|
|
if (typeof data.length == 'number') {
|
|
this.setLength(this.length)
|
|
}
|
|
if (data.bones && !data.animators) {
|
|
data.animators = data.bones;
|
|
}
|
|
if (data.animators instanceof Object) {
|
|
for (var key in data.animators) {
|
|
var group = Group.all.findInArray( isUUID(key) ? 'uuid' : 'name', key )
|
|
if (group) {
|
|
var ba = this.getBoneAnimator(group)
|
|
var kfs = data.animators[key]
|
|
if (kfs && ba) {
|
|
ba.rotation.empty();
|
|
ba.position.empty();
|
|
ba.scale.empty();
|
|
kfs.forEach(kf_data => {
|
|
ba.addKeyframe(kf_data, kf_data.uuid);
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (data.markers instanceof Array) {
|
|
data.markers.forEach(marker => {
|
|
this.markers.push(new TimelineMarker(marker));
|
|
})
|
|
}
|
|
return this;
|
|
}
|
|
getUndoCopy(options, save) {
|
|
var scope = this;
|
|
var copy = {
|
|
uuid: this.uuid,
|
|
name: this.name,
|
|
loop: this.loop,
|
|
override: this.override,
|
|
anim_time_update: this.anim_time_update,
|
|
length: this.length,
|
|
selected: this.selected,
|
|
}
|
|
if (Object.keys(this.animators).length) {
|
|
copy.animators = {}
|
|
for (var uuid in this.animators) {
|
|
var kfs = this.animators[uuid].keyframes
|
|
if (kfs && kfs.length) {
|
|
if (options && options.bone_names && this.animators[uuid] instanceof BoneAnimator) {
|
|
var group = this.animators[uuid].getGroup();
|
|
uuid = group ? group.name : '';
|
|
}
|
|
copy.animators[uuid] = [];
|
|
kfs.forEach(kf => {
|
|
copy.animators[uuid].push(kf.getUndoCopy(save));
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return copy;
|
|
}
|
|
select() {
|
|
var scope = this;
|
|
Prop.active_panel = 'animations';
|
|
if (this == Animator.selected) return;
|
|
var selected_bone = Group.selected;
|
|
Animator.animations.forEach(function(a) {
|
|
a.selected = a.playing = false;
|
|
})
|
|
Timeline.animators.purge();
|
|
Timeline.selected.empty();
|
|
Timeline.vue._data.markers = this.markers;
|
|
Timeline.vue._data.animation_length = this.length;
|
|
this.selected = true;
|
|
this.playing = true;
|
|
Animator.selected = this;
|
|
unselectAll();
|
|
BarItems.slider_animation_length.update();
|
|
|
|
Group.all.forEach(group => {
|
|
scope.getBoneAnimator(group);
|
|
})
|
|
|
|
if (selected_bone) {
|
|
selected_bone.select();
|
|
}
|
|
Animator.preview();
|
|
return this;
|
|
}
|
|
setLength(len) {
|
|
len = limitNumber(len, 0, 1e4)
|
|
this.length = len;
|
|
if (Animator.selected == this) {
|
|
Timeline.vue._data.animation_length = this.length;
|
|
BarItems.slider_animation_length.update()
|
|
}
|
|
}
|
|
createUniqueName(arr) {
|
|
var scope = this;
|
|
var others = Animator.animations;
|
|
if (arr && arr.length) {
|
|
arr.forEach(g => {
|
|
others.safePush(g)
|
|
})
|
|
}
|
|
var name = this.name.replace(/\d+$/, '');
|
|
function check(n) {
|
|
for (var i = 0; i < others.length; i++) {
|
|
if (others[i] !== scope && others[i].name == n) return false;
|
|
}
|
|
return true;
|
|
}
|
|
if (check(this.name)) {
|
|
return this.name;
|
|
}
|
|
for (var num = 2; num < 8e3; num++) {
|
|
if (check(name+num)) {
|
|
scope.name = name+num;
|
|
return scope.name;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
rename() {
|
|
var scope = this;
|
|
Blockbench.textPrompt('generic.rename', this.name, function(name) {
|
|
if (name && name !== scope.name) {
|
|
Undo.initEdit({animations: [scope]});
|
|
scope.name = name;
|
|
scope.createUniqueName();
|
|
Undo.finishEdit('rename animation');
|
|
}
|
|
})
|
|
return this;
|
|
}
|
|
editUpdateVariable() {
|
|
var scope = this;
|
|
Blockbench.textPrompt('message.animation_update_var', scope.anim_time_update, function(name) {
|
|
if (name !== scope.anim_time_update) {
|
|
Undo.initEdit({animations: [scope]})
|
|
scope.anim_time_update = name
|
|
Undo.finishEdit('change animation variable')
|
|
}
|
|
})
|
|
return this;
|
|
}
|
|
togglePlayingState(state) {
|
|
if (!this.selected) {
|
|
this.playing = state !== undefined ? state : !this.playing;
|
|
Animator.preview();
|
|
} else {
|
|
Timeline.start();
|
|
}
|
|
return this.playing;
|
|
}
|
|
showContextMenu(event) {
|
|
this.select();
|
|
this.menu.open(event, this);
|
|
return this;
|
|
}
|
|
getBoneAnimator(group) {
|
|
if (!group && Group.selected) {
|
|
group = Group.selected
|
|
} else if (!group) {
|
|
return;
|
|
}
|
|
var uuid = group.uuid
|
|
if (!this.animators[uuid]) {
|
|
this.animators[uuid] = new BoneAnimator(uuid, this);
|
|
}
|
|
return this.animators[uuid];
|
|
}
|
|
add(undo) {
|
|
if (undo) {
|
|
Undo.initEdit({animations: []})
|
|
}
|
|
if (!Animator.animations.includes(this)) {
|
|
Animator.animations.push(this)
|
|
}
|
|
if (undo) {
|
|
this.select()
|
|
Undo.finishEdit('add animation', {animations: [this]})
|
|
}
|
|
return this;
|
|
}
|
|
remove(undo) {
|
|
if (undo) {
|
|
Undo.initEdit({animations: [this]})
|
|
}
|
|
if (Animator.selected === this) {
|
|
Animator.selected = false
|
|
}
|
|
Animator.animations.remove(this)
|
|
if (undo) {
|
|
Undo.finishEdit('remove animation', {animation: null})
|
|
}
|
|
Blockbench.dispatchEvent('remove_animation', {animations: [this]})
|
|
return this;
|
|
}
|
|
getMaxLength() {
|
|
var len = this.length||0
|
|
|
|
for (var uuid in this.animators) {
|
|
var bone = this.animators[uuid]
|
|
var keyframes = bone.keyframes;
|
|
var i = 0;
|
|
while (i < keyframes.length) {
|
|
len = Math.max(len, keyframes[i].time)
|
|
i++;
|
|
}
|
|
}
|
|
this.setLength(len)
|
|
if (this == Animator.selected) {
|
|
BarItems.slider_animation_length.update()
|
|
}
|
|
return len
|
|
}
|
|
setLoop(value, undo) {
|
|
if ((value == 'once' || value == 'loop' || value == 'hold') && value !== this.loop) {
|
|
if (undo) Undo.initEdit({animations: [this]})
|
|
this.loop = value;
|
|
if (undo) Undo.finishEdit('change animation loop mode')
|
|
}
|
|
}
|
|
}
|
|
Animation.prototype.menu = new Menu([
|
|
{name: 'menu.animation.loop', icon: 'loop', children: [
|
|
{name: 'menu.animation.loop.once', icon: animation => (animation.loop == 'once' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('once', true)}},
|
|
{name: 'menu.animation.loop.hold', icon: animation => (animation.loop == 'hold' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('hold', true)}},
|
|
{name: 'menu.animation.loop.loop', icon: animation => (animation.loop == 'loop' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('loop', true)}},
|
|
]},
|
|
{name: 'menu.animation.override', icon: (a) => (a.override?'check_box':'check_box_outline_blank'), click: function(animation) {
|
|
animation.override = !animation.override
|
|
}},
|
|
{name: 'menu.animation.anim_time_update', icon: 'update', click: function(animation) {
|
|
animation.editUpdateVariable()
|
|
}},
|
|
'_',
|
|
'duplicate',
|
|
'rename',
|
|
'delete',
|
|
])
|
|
class TimelineMarker {
|
|
constructor(data) {
|
|
this.time = 0;
|
|
this.color = 0;
|
|
if (data) {
|
|
this.extend(data);
|
|
}
|
|
}
|
|
extend(data) {
|
|
Merge.number(this, data, 'color');
|
|
Merge.number(this, data, 'time');
|
|
}
|
|
callPlayhead() {
|
|
Timeline.setTime(this.time)
|
|
Animator.preview()
|
|
return this;
|
|
}
|
|
showContextMenu(event) {
|
|
this.menu.open(event, this);
|
|
return this;
|
|
}
|
|
}
|
|
TimelineMarker.prototype.menu = new Menu([
|
|
{icon: 'flag', color: markerColors[0].standard, name: 'cube.color.'+markerColors[0].name, click: function(marker) {marker.color = 0;}},
|
|
{icon: 'flag', color: markerColors[1].standard, name: 'cube.color.'+markerColors[1].name, click: function(marker) {marker.color = 1;}},
|
|
{icon: 'flag', color: markerColors[2].standard, name: 'cube.color.'+markerColors[2].name, click: function(marker) {marker.color = 2;}},
|
|
{icon: 'flag', color: markerColors[3].standard, name: 'cube.color.'+markerColors[3].name, click: function(marker) {marker.color = 3;}},
|
|
{icon: 'flag', color: markerColors[4].standard, name: 'cube.color.'+markerColors[4].name, click: function(marker) {marker.color = 4;}},
|
|
{icon: 'flag', color: markerColors[5].standard, name: 'cube.color.'+markerColors[5].name, click: function(marker) {marker.color = 5;}},
|
|
{icon: 'flag', color: markerColors[6].standard, name: 'cube.color.'+markerColors[6].name, click: function(marker) {marker.color = 6;}},
|
|
{icon: 'flag', color: markerColors[7].standard, name: 'cube.color.'+markerColors[7].name, click: function(marker) {marker.color = 7;}},
|
|
{icon: 'delete', name: 'generic.delete', click: function(marker) {
|
|
if (Animator.selected) Animator.selected.markers.remove(marker);
|
|
}}
|
|
])
|
|
class GeneralAnimator {
|
|
constructor(uuid, animation) {
|
|
this.animation = animation;
|
|
this.expanded = false;
|
|
this.selected = false;
|
|
this.uuid = uuid || guid();
|
|
this.muted = {};
|
|
this.channels.forEach(channel => {
|
|
this.muted[channel] = false;
|
|
})
|
|
}
|
|
select() {
|
|
var scope = this;
|
|
TickUpdates.keyframes = true;
|
|
for (var key in Animator.selected.animators) {
|
|
Animator.selected.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);
|
|
}
|
|
if (!this.expanded) this.expanded = true;
|
|
return this;
|
|
}
|
|
addKeyframe(data, uuid) {
|
|
var channel = data.channel;
|
|
if (typeof channel == 'number') channel = this.channels[channel];
|
|
if (channel && this[channel]) {
|
|
var kf = new Keyframe(data, uuid);
|
|
this[channel].push(kf);
|
|
kf.animator = this;
|
|
return kf;
|
|
}
|
|
}
|
|
createKeyframe(value, time, channel, undo, select) {
|
|
if (!this[channel]) return;
|
|
var keyframes = [];
|
|
if (undo) {
|
|
Undo.initEdit({keyframes})
|
|
}
|
|
var keyframe = new Keyframe({
|
|
channel: channel,
|
|
time: time
|
|
});
|
|
keyframes.push(keyframe);
|
|
|
|
if (value) {
|
|
keyframe.extend(value);
|
|
} else if (this.fillValues) {
|
|
this.fillValues(keyframe, value, true);
|
|
}
|
|
keyframe.channel = channel;
|
|
keyframe.time = time;
|
|
|
|
this[channel].push(keyframe);
|
|
keyframe.animator = this;
|
|
|
|
if (select !== false) {
|
|
keyframe.select();
|
|
}
|
|
TickUpdates.keyframes = true;
|
|
|
|
var deleted = [];
|
|
delete keyframe.time_before;
|
|
keyframe.replaceOthers(deleted);
|
|
Undo.addKeyframeCasualties(deleted);
|
|
|
|
if (undo) {
|
|
Undo.finishEdit('added keyframe')
|
|
}
|
|
return keyframe;
|
|
}
|
|
getOrMakeKeyframe(channel) {
|
|
var before, result;
|
|
|
|
for (var kf of this[channel]) {
|
|
if (Math.abs(kf.time - Timeline.time) < 0.02) {
|
|
before = kf;
|
|
}
|
|
}
|
|
result = before ? before : this.createKeyframe(null, Timeline.time, channel, false, false);
|
|
return {before, result};
|
|
}
|
|
toggleMuted(channel) {
|
|
this.muted[channel] = !this.muted[channel];
|
|
}
|
|
scrollTo() {
|
|
var el = $(`#timeline_body_inner > li[uuid=${this.uuid}]`).get(0)
|
|
if (el) {
|
|
var offset = el.offsetTop;
|
|
var timeline = $('#timeline_body').scrollTop();
|
|
var height = $('#timeline_body').height();
|
|
if (offset < timeline) {
|
|
$('#timeline_body').animate({
|
|
scrollTop: offset
|
|
}, 200);
|
|
}
|
|
if (offset + el.clientHeight > timeline + height) {
|
|
$('#timeline_body').animate({
|
|
scrollTop: offset - (height-el.clientHeight-20)
|
|
}, 200);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class BoneAnimator extends GeneralAnimator {
|
|
constructor(uuid, animation) {
|
|
super(uuid, animation);
|
|
this.uuid = uuid;
|
|
|
|
this.rotation = [];
|
|
this.position = [];
|
|
this.scale = [];
|
|
}
|
|
get name() {
|
|
var group = this.getGroup();
|
|
if (group) return group.name;
|
|
return '';
|
|
}
|
|
get keyframes() {
|
|
return [...this.rotation, ...this.position, ...this.scale];
|
|
}
|
|
getGroup() {
|
|
this.group = Group.all.findInArray('uuid', this.uuid)
|
|
if (!this.group) {
|
|
console.log('no group found for '+this.uuid)
|
|
if (this.animation && this.animation.animators[this.uuid] && this.animation.animators[this.uuid].type == 'bone') {
|
|
delete this.animation.bones[this.uuid];
|
|
}
|
|
}
|
|
return this.group
|
|
}
|
|
select(group_is_selected) {
|
|
if (this.getGroup().locked) return this;
|
|
|
|
var duplicates;
|
|
for (var key in this.animation.animators) {
|
|
this.animation.animators[key].selected = false;
|
|
}
|
|
if (group_is_selected !== true) {
|
|
this.group.select();
|
|
}
|
|
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',
|
|
buttons: ['dialog.ok'],
|
|
});
|
|
}
|
|
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) {
|
|
|
|
if (values instanceof Array) {
|
|
keyframe.extend({
|
|
x: values[0],
|
|
y: values[1],
|
|
z: values[2]
|
|
})
|
|
if (values[3]) {
|
|
keyframe.extend({w: values[3], isQuaternion: true})
|
|
}
|
|
} else if (typeof values === 'number' || typeof values === 'string') {
|
|
keyframe.extend({
|
|
x: values,
|
|
y: values,
|
|
z: values
|
|
})
|
|
} else if (values == null) {
|
|
var ref = this.interpolate(keyframe.channel, allow_expression)
|
|
if (ref) {
|
|
let e = 1e2
|
|
ref.forEach((r, i) => {
|
|
if (!isNaN(r)) {
|
|
ref[i] = Math.round(parseFloat(r)*e)/e
|
|
}
|
|
})
|
|
keyframe.extend({
|
|
x: ref[0],
|
|
y: ref[1],
|
|
z: ref[2],
|
|
w: ref.length === 4 ? ref[3] : undefined,
|
|
isQuaternion: ref.length === 4
|
|
})
|
|
}
|
|
} 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) {
|
|
var bone = this.group.mesh
|
|
|
|
if (!arr) {
|
|
} else if (arr.length === 4) {
|
|
var added_rotation = new THREE.Euler().setFromQuaternion(new THREE.Quaternion().fromArray(arr), 'ZYX')
|
|
bone.rotation.x -= added_rotation.x
|
|
bone.rotation.y -= added_rotation.y
|
|
bone.rotation.z += added_rotation.z
|
|
} else {
|
|
arr.forEach((n, i) => {
|
|
bone.rotation[getAxisLetter(i)] += Math.degToRad(n) * (i == 2 ? 1 : -1)
|
|
})
|
|
}
|
|
return this;
|
|
}
|
|
displayPosition(arr) {
|
|
var bone = this.group.mesh
|
|
if (arr) {
|
|
bone.position.x += -arr[0];
|
|
bone.position.y += arr[1];
|
|
bone.position.z += arr[2];
|
|
}
|
|
return this;
|
|
}
|
|
displayScale(arr) {
|
|
if (!arr) return this;
|
|
var bone = this.group.mesh;
|
|
bone.scale.x *= arr[0] || 0.00001;
|
|
bone.scale.y *= arr[1] || 0.00001;
|
|
bone.scale.z *= arr[2] || 0.00001;
|
|
return this;
|
|
}
|
|
interpolate(channel, allow_expression) {
|
|
let time = Timeline.time;
|
|
var i = 0;
|
|
var before = false
|
|
var after = false
|
|
var result = false
|
|
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.abs(before.time - time) < 1/1200) {
|
|
result = before
|
|
} else if (after && Math.abs(after.time - time) < 1/1200) {
|
|
result = after
|
|
} else if (before && !after) {
|
|
result = before
|
|
} else if (after && !before) {
|
|
result = after
|
|
} else if (!before && !after) {
|
|
//
|
|
} else {
|
|
let alpha = Math.lerp(before.time, after.time, time)
|
|
if (Blockbench.hasFlag('no_interpolations')) {
|
|
alpha = Math.round(alpha)
|
|
}
|
|
result = [
|
|
before.getLerp(after, 'x', alpha, allow_expression),
|
|
before.getLerp(after, 'y', alpha, allow_expression),
|
|
before.getLerp(after, 'z', alpha, allow_expression)
|
|
]
|
|
if (before.isQuaternion && after.isQuaternion) {
|
|
result[3] = before.getLerp(after, 'q', alpha, allow_expression)
|
|
}
|
|
}
|
|
if (result && result.type === 'keyframe') {
|
|
let keyframe = result
|
|
let method = allow_expression ? 'get' : 'calc'
|
|
result = [
|
|
keyframe[method]('x'),
|
|
keyframe[method]('y'),
|
|
keyframe[method]('z')
|
|
]
|
|
if (keyframe.isQuaternion) {
|
|
result[3] = keyframe[method]('w')
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
displayFrame() {
|
|
if (!this.doRender()) return;
|
|
this.getGroup()
|
|
|
|
if (!this.muted.rotation) this.displayRotation(this.interpolate('rotation'))
|
|
if (!this.muted.position) this.displayPosition(this.interpolate('position'))
|
|
if (!this.muted.scale) this.displayScale(this.interpolate('scale'))
|
|
}
|
|
}
|
|
BoneAnimator.prototype.channels = ['rotation', 'position', 'scale']
|
|
class EffectAnimator extends GeneralAnimator {
|
|
constructor(animation) {
|
|
super(null, animation);
|
|
|
|
this.name = tl('timeline.effects')
|
|
this.selected = false;
|
|
|
|
this.particle = [];
|
|
this.sound = [];
|
|
this.timeline = [];
|
|
}
|
|
get keyframes() {
|
|
return [...this.particle, ...this.sound, ...this.timeline];
|
|
}
|
|
pushKeyframe(keyframe) {
|
|
this[keyframe.channel].push(keyframe)
|
|
keyframe.animator = this;
|
|
return this;
|
|
}
|
|
displayFrame() {
|
|
if (!this.muted.sound) {
|
|
this.sound.forEach(kf => {
|
|
var diff = kf.time - Timeline.time;
|
|
if (diff >= 0 && diff < (1/60) * (Timeline.playback_speed/100)) {
|
|
if (kf.file && !kf.cooldown) {
|
|
var media = new Audio(kf.file);
|
|
window._media = media
|
|
media.volume = Math.clamp(settings.volume.value/100, 0, 1);
|
|
media.play();
|
|
Timeline.playing_sounds.push(media);
|
|
media.onended = function() {
|
|
Timeline.playing_sounds.remove(media);
|
|
}
|
|
|
|
kf.cooldown = true;
|
|
setTimeout(() => {
|
|
delete kf.cooldown;
|
|
}, 400)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
EffectAnimator.prototype.channels = ['particle', 'sound', 'timeline']
|
|
class Keyframe {
|
|
constructor(data, uuid) {
|
|
this.type = 'keyframe'
|
|
this.channel = 'rotation';
|
|
this.time = 0;
|
|
this.selected = 0;
|
|
this.x = '0';
|
|
this.y = '0';
|
|
this.z = '0';
|
|
this.w = '0';
|
|
this.isQuaternion = false;
|
|
this.effect = '';
|
|
this.file = '';
|
|
this.locator = '';
|
|
this.script = '';
|
|
this.instructions = '';
|
|
this.uuid = (uuid && isUUID(uuid)) ? uuid : guid();
|
|
if (typeof data === 'object') {
|
|
Merge.string(this, data, 'channel')
|
|
this.transform = this.channel === 'rotation' || this.channel === 'position' || this.channel === 'scale';
|
|
this.extend(data)
|
|
if (this.channel === 'scale' && data.x == undefined && data.y == undefined && data.z == undefined) {
|
|
this.x = this.y = this.z = 1;
|
|
}
|
|
}
|
|
}
|
|
extend(data) {
|
|
Merge.number(this, data, 'time')
|
|
|
|
if (this.transform) {
|
|
if (data.values != undefined) {
|
|
if (typeof data.values == 'number' || typeof data.values == 'string') {
|
|
data.x = data.y = data.z = data.values;
|
|
|
|
} else if (data.values instanceof Array) {
|
|
data.x = data.values[0];
|
|
data.y = data.values[1];
|
|
data.z = data.values[2];
|
|
data.w = data.values[3];
|
|
}
|
|
}
|
|
Merge.string(this, data, 'x')
|
|
Merge.string(this, data, 'y')
|
|
Merge.string(this, data, 'z')
|
|
Merge.string(this, data, 'w')
|
|
Merge.boolean(this, data, 'isQuaternion')
|
|
} else {
|
|
if (data.values) {
|
|
data.effect = data.values.effect;
|
|
data.locator = data.values.locator;
|
|
data.script = data.values.script;
|
|
data.file = data.values.file;
|
|
data.instructions = data.values.instructions;
|
|
}
|
|
Merge.string(this, data, 'effect')
|
|
Merge.string(this, data, 'locator')
|
|
Merge.string(this, data, 'script')
|
|
Merge.string(this, data, 'file')
|
|
Merge.string(this, data, 'instructions')
|
|
}
|
|
return this;
|
|
}
|
|
get(axis) {
|
|
if (!this[axis]) {
|
|
return this.transform ? 0 : '';
|
|
} else if (!isNaN(this[axis])) {
|
|
return parseFloat(this[axis])
|
|
} else {
|
|
return this[axis]
|
|
}
|
|
}
|
|
calc(axis) {
|
|
return Molang.parse(this[axis])
|
|
}
|
|
set(axis, value) {
|
|
this[axis] = value;
|
|
return this;
|
|
}
|
|
offset(axis, amount) {
|
|
var value = this.get(axis)
|
|
if (!value || value === '0') {
|
|
this.set(axis, amount)
|
|
return amount;
|
|
}
|
|
if (typeof value === 'number') {
|
|
this.set(axis, value+amount)
|
|
return value+amount
|
|
}
|
|
var start = value.match(/^-?\s*\d*(\.\d+)?\s*(\+|-)/)
|
|
if (start) {
|
|
var number = parseFloat( start[0].substr(0, start[0].length-1) ) + amount
|
|
value = trimFloatNumber(number) + value.substr(start[0].length-1)
|
|
} else {
|
|
|
|
var end = value.match(/(\+|-)\s*\d*(\.\d+)?\s*$/)
|
|
if (end) {
|
|
var number = (parseFloat( end[0] ) + amount)
|
|
value = value.substr(0, end.index) + ((number.toString()).substr(0,1)=='-'?'':'+') + trimFloatNumber(number)
|
|
} else {
|
|
value = trimFloatNumber(amount) +(value.substr(0,1)=='-'?'':'+')+ value
|
|
}
|
|
}
|
|
this.set(axis, value)
|
|
return value;
|
|
}
|
|
flip(axis) {
|
|
if (!this.transform) return this;
|
|
function negate(value) {
|
|
if (!value || value === '0') {
|
|
return value;
|
|
}
|
|
if (typeof value === 'number') {
|
|
return -value;
|
|
}
|
|
var start = value.match(/^-?\s*\d*(\.\d+)?\s*(\+|-)/)
|
|
if (start) {
|
|
var number = parseFloat( start[0].substr(0, start[0].length-1) );
|
|
return trimFloatNumber(-number) + value.substr(start[0].length-1);
|
|
} else {
|
|
return `-(${value})`;
|
|
}
|
|
}
|
|
if (this.channel == 'rotation') {
|
|
for (var i = 0; i < 3; i++) {
|
|
if (i != axis) {
|
|
let l = getAxisLetter(i)
|
|
this.set(l, negate(this.get(l)))
|
|
}
|
|
}
|
|
} else if (this.channel == 'position') {
|
|
let l = getAxisLetter(axis)
|
|
this.set(l, negate(this.get(l)))
|
|
}
|
|
return this;
|
|
}
|
|
getLerp(other, axis, amount, allow_expression) {
|
|
if (allow_expression && this.get(axis) === other.get(axis)) {
|
|
return this.get(axis)
|
|
} else {
|
|
let calc = this.calc(axis)
|
|
return calc + (other.calc(axis) - calc) * amount
|
|
}
|
|
}
|
|
getArray() {
|
|
var arr = [
|
|
this.get('x'),
|
|
this.get('y'),
|
|
this.get('z'),
|
|
]
|
|
return arr;
|
|
}
|
|
getFixed() {
|
|
if (this.channel === 'rotation') {
|
|
let fix = this.animator.group.mesh.fix_rotation;
|
|
return new THREE.Quaternion().setFromEuler(new THREE.Euler(
|
|
fix.x - Math.degToRad(this.calc('x')),
|
|
fix.y - Math.degToRad(this.calc('y')),
|
|
fix.z + Math.degToRad(this.calc('z')),
|
|
'ZYX'
|
|
));
|
|
} else if (this.channel === 'position') {
|
|
let fix = this.animator.group.mesh.fix_position;
|
|
return new THREE.Vector3(
|
|
fix.x - this.calc('x'),
|
|
fix.y + this.calc('y'),
|
|
fix.z + this.calc('z')
|
|
)
|
|
} else if (this.channel === 'scale') {
|
|
return new THREE.Vector3(
|
|
this.calc('x'),
|
|
this.calc('y'),
|
|
this.calc('z')
|
|
)
|
|
}
|
|
}
|
|
replaceOthers(save) {
|
|
var scope = this;
|
|
var arr = this.animator[this.channel];
|
|
var replaced;
|
|
arr.forEach(kf => {
|
|
if (kf != scope && Math.abs(kf.time - scope.time) < 0.0001) {
|
|
save.push(kf);
|
|
kf.remove();
|
|
}
|
|
})
|
|
}
|
|
select(event) {
|
|
var scope = this;
|
|
if (Timeline.dragging_keyframes) {
|
|
Timeline.dragging_keyframes = false
|
|
return this;
|
|
}
|
|
if (!event || (!event.shiftKey && !event.ctrlOrCmd)) {
|
|
Timeline.selected.forEach(function(kf) {
|
|
kf.selected = false
|
|
})
|
|
Timeline.selected.empty()
|
|
}
|
|
if (event && event.shiftKey && Timeline.selected.length) {
|
|
var last = Timeline.selected[Timeline.selected.length-1]
|
|
if (last && last.channel === scope.channel && last.animator == scope.animator) {
|
|
Timeline.keyframes.forEach((kf) => {
|
|
if (kf.channel === scope.channel &&
|
|
kf.animator === scope.animator &&
|
|
Math.isBetween(kf.time, last.time, scope.time) &&
|
|
!kf.selected
|
|
) {
|
|
kf.selected = true
|
|
Timeline.selected.push(kf)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
Timeline.selected.safePush(this);
|
|
if (Timeline.selected.length == 1 && Timeline.selected[0].animator.selected == false) {
|
|
Timeline.selected[0].animator.select()
|
|
}
|
|
|
|
var select_tool = true;
|
|
Timeline.selected.forEach(kf => {
|
|
if (kf.channel != scope.channel) select_tool = false;
|
|
})
|
|
this.selected = true
|
|
TickUpdates.keyframe_selection = true;
|
|
if (select_tool) {
|
|
switch (this.channel) {
|
|
case 'rotation': BarItems.rotate_tool.select(); break;
|
|
case 'position': BarItems.move_tool.select(); break;
|
|
case 'scale': BarItems.resize_tool.select(); break;
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
callPlayhead() {
|
|
Timeline.setTime(this.time)
|
|
Animator.preview()
|
|
return this;
|
|
}
|
|
showContextMenu(event) {
|
|
if (!this.selected) {
|
|
this.select();
|
|
}
|
|
this.menu.open(event, this);
|
|
return this;
|
|
}
|
|
remove() {
|
|
if (this.animator) {
|
|
this.animator[this.channel].remove(this)
|
|
}
|
|
Timeline.selected.remove(this)
|
|
}
|
|
getUndoCopy(save) {
|
|
var copy = {
|
|
animator: save ? undefined : this.animator && this.animator.uuid,
|
|
uuid: save && this.uuid,
|
|
channel: this.channel,
|
|
time: this.time,
|
|
x: this.x,
|
|
y: this.y,
|
|
z: this.z,
|
|
}
|
|
if (this.transform) {
|
|
copy.x = this.x;
|
|
copy.y = this.y;
|
|
copy.z = this.z;
|
|
if (this.channel == 'rotation' && this.isQuaternion) {
|
|
copy.w = this.w
|
|
}
|
|
} else if (this.channel == 'particle') {
|
|
copy.effect = this.effect;
|
|
copy.locator = this.locator;
|
|
copy.script = this.script;
|
|
|
|
} else if (this.channel == 'sound') {
|
|
copy.effect = this.effect;
|
|
copy.file = this.file;
|
|
|
|
} else if (this.channel == 'timeline') {
|
|
copy.instructions = this.instructions;
|
|
}
|
|
return copy;
|
|
}
|
|
}
|
|
Keyframe.prototype.menu = new Menu([
|
|
//Quaternions have been removed in Bedrock 1.10.0
|
|
/*
|
|
{name: 'menu.keyframe.quaternion',
|
|
icon: (keyframe) => (keyframe.isQuaternion ? 'check_box' : 'check_box_outline_blank'),
|
|
condition: (keyframe) => keyframe.channel === 'rotation',
|
|
click: function(keyframe) {
|
|
keyframe.select()
|
|
var state = !keyframe.isQuaternion
|
|
Timeline.keyframes.forEach((kf) => {
|
|
kf.isQuaternion = state
|
|
})
|
|
updateKeyframeSelection()
|
|
}
|
|
},*/
|
|
'change_keyframe_file',
|
|
'_',
|
|
'copy',
|
|
'delete',
|
|
])
|
|
//Misc Functions
|
|
function updateKeyframeValue(obj) {
|
|
var axis = $(obj).attr('axis');
|
|
var value = $(obj).val();
|
|
Timeline.selected.forEach(function(kf) {
|
|
kf.set(axis, value);
|
|
})
|
|
if (!['effect', 'locator', 'script'].includes(axis)) {
|
|
Animator.preview();
|
|
}
|
|
}
|
|
function updateKeyframeSelection() {
|
|
var multi_channel = false;
|
|
var channel = false;
|
|
Timeline.selected.forEach((kf) => {
|
|
if (channel === false) {
|
|
channel = kf.channel
|
|
} else if (channel !== kf.channel) {
|
|
multi_channel = true
|
|
}
|
|
})
|
|
$('.panel#keyframe .bar').hide();
|
|
|
|
if (Timeline.selected.length && !multi_channel) {
|
|
var first = Timeline.selected[0]
|
|
|
|
$('#keyframe_type_label').text(tl('panel.keyframe.type', [tl('timeline.'+first.channel)] ))
|
|
|
|
if (first.animator instanceof BoneAnimator) {
|
|
function _gt(axis) {
|
|
var n = first.get(axis);
|
|
if (typeof n == 'number') return trimFloatNumber(n);
|
|
return n;
|
|
}
|
|
$('#keyframe_bar_x, #keyframe_bar_y, #keyframe_bar_z').show();
|
|
$('#keyframe_bar_w').toggle(first.channel === 'rotation' && first.isQuaternion)
|
|
|
|
$('#keyframe_bar_x input').val(_gt('x'));
|
|
$('#keyframe_bar_y input').val(_gt('y'));
|
|
$('#keyframe_bar_z input').val(_gt('z'));
|
|
if (first.channel === 'rotation' && first.isQuaternion) {
|
|
$('#keyframe_bar_w input').val(_gt('w'));
|
|
}
|
|
} else if (first.channel == 'particle') {
|
|
$('#keyframe_bar_effect').show();
|
|
$('#keyframe_bar_effect input').val(first.get('effect'));
|
|
$('#keyframe_bar_locator').show();
|
|
$('#keyframe_bar_locator input').val(first.get('locator'));
|
|
$('#keyframe_bar_script').show();
|
|
$('#keyframe_bar_script input').val(first.get('script'));
|
|
|
|
} else if (first.channel == 'sound') {
|
|
$('#keyframe_bar_effect').show();
|
|
$('#keyframe_bar_effect input').val(first.get('effect'));
|
|
|
|
} else if (first.channel == 'timeline') {
|
|
$('#keyframe_bar_instructions').show();
|
|
$('#keyframe_bar_instructions textarea').val(first.get('instructions'));
|
|
}
|
|
BarItems.slider_keyframe_time.update()
|
|
} else {
|
|
$('#keyframe_type_label').text('')
|
|
$('#keyframe_bar_x, #keyframe_bar_y, #keyframe_bar_z, #keyframe_bar_w').hide()
|
|
}
|
|
BARS.updateConditions()
|
|
Blockbench.dispatchEvent('update_keyframe_selection');
|
|
}
|
|
function selectAllKeyframes() {
|
|
if (!Animator.selected) return;
|
|
var state = Timeline.selected.length !== Timeline.keyframes.length
|
|
Timeline.keyframes.forEach((kf) => {
|
|
if (state && !kf.selected) {
|
|
Timeline.selected.push(kf)
|
|
} else if (!state && kf.selected) {
|
|
Timeline.selected.remove(kf)
|
|
}
|
|
kf.selected = state
|
|
})
|
|
updateKeyframeSelection()
|
|
}
|
|
function removeSelectedKeyframes() {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
var i = Timeline.keyframes.length;
|
|
while (i > 0) {
|
|
i--;
|
|
let kf = Timeline.keyframes[i]
|
|
if (Timeline.selected.includes(kf)) {
|
|
kf.remove()
|
|
}
|
|
}
|
|
updateKeyframeSelection()
|
|
Animator.preview()
|
|
Undo.finishEdit('remove keyframes')
|
|
}
|
|
function findBedrockAnimation() {
|
|
var animation_path = ModelMeta.export_path.split(osfs)
|
|
var index = animation_path.lastIndexOf('models')
|
|
animation_path.splice(index)
|
|
var path1 = [...animation_path, 'animations', pathToName(ModelMeta.export_path)+'.json'].join(osfs)
|
|
var path2 = [...animation_path, 'animations', pathToName(ModelMeta.export_path).replace('.geo', '')+'.animation.json'].join(osfs)
|
|
if (fs.existsSync(path1)) {
|
|
Blockbench.read([path1], {}, (files) => {
|
|
Animator.loadFile(files[0])
|
|
})
|
|
} else if (fs.existsSync(path2)) {
|
|
Blockbench.read([path2], {}, (files) => {
|
|
Animator.loadFile(files[0])
|
|
})
|
|
}
|
|
}
|
|
//Clipbench
|
|
Clipbench.setKeyframes = function() {
|
|
|
|
var keyframes = Timeline.selected;
|
|
|
|
Clipbench.keyframes = []
|
|
if (!keyframes || keyframes.length === 0) {
|
|
return;
|
|
}
|
|
var first = keyframes[0];
|
|
var single_animator;
|
|
keyframes.forEach(function(kf) {
|
|
if (kf.time < first.time) {
|
|
first = kf
|
|
}
|
|
if (single_animator && single_animator !== kf.animator.uuid) {
|
|
single_animator = false;
|
|
} else if (single_animator == undefined) {
|
|
single_animator = kf.animator.uuid;
|
|
}
|
|
})
|
|
|
|
keyframes.forEach(function(kf) {
|
|
var copy = kf.getUndoCopy();
|
|
copy.time_offset = kf.time - first.time;
|
|
if (single_animator != false) {
|
|
delete copy.animator;
|
|
}
|
|
Clipbench.keyframes.push(copy)
|
|
})
|
|
if (isApp) {
|
|
clipboard.writeHTML(JSON.stringify({type: 'keyframes', content: Clipbench.keyframes}))
|
|
}
|
|
}
|
|
Clipbench.pasteKeyframes = function() {
|
|
if (isApp) {
|
|
var raw = clipboard.readHTML()
|
|
try {
|
|
var data = JSON.parse(raw)
|
|
if (data.type === 'keyframes' && data.content) {
|
|
Clipbench.keyframes = data.content
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
if (Clipbench.keyframes && Clipbench.keyframes.length) {
|
|
|
|
if (!Animator.selected) return;
|
|
var keyframes = [];
|
|
Undo.initEdit({keyframes});
|
|
Timeline.selected.empty();
|
|
Timeline.keyframes.forEach((kf) => {
|
|
kf.selected = false;
|
|
})
|
|
Clipbench.keyframes.forEach(function(data, i) {
|
|
|
|
if (data.animator) {
|
|
var animator = Animator.selected.animators[data.animator];
|
|
if (animator && !Timeline.animators.includes(animator)) {
|
|
animator.addToTimeline();
|
|
}
|
|
} else {
|
|
var animator = Timeline.selected_animator;
|
|
}
|
|
if (animator) {
|
|
var kf = animator.createKeyframe(data, Timeline.time + data.time_offset, data.channel, false, false)
|
|
keyframes.push(kf);
|
|
kf.selected = true;
|
|
Timeline.selected.push(kf);
|
|
}
|
|
|
|
})
|
|
TickUpdates.keyframe_selection = true;
|
|
Animator.preview()
|
|
Undo.finishEdit('paste keyframes');
|
|
}
|
|
}
|
|
|
|
const Animator = {
|
|
possible_channels: {rotation: true, position: true, scale: true, sound: true, particle: true, timeline: true},
|
|
open: false,
|
|
animations: [],
|
|
frame: 0,
|
|
interval: false,
|
|
join() {
|
|
|
|
Animator.open = true;
|
|
selected.empty()
|
|
Canvas.updateAllBones()
|
|
|
|
//if (quad_previews.enabled) {
|
|
// quad_previews.enabled_before = true
|
|
//}
|
|
//main_preview.fullscreen()
|
|
//main_preview.setNormalCamera()
|
|
|
|
$('body').addClass('animation_mode')
|
|
if (!Animator.timeline_node) {
|
|
Animator.timeline_node = $('#timeline').get(0)
|
|
}
|
|
updateInterface()
|
|
Toolbars.element_origin.toPlace('bone_origin')
|
|
if (!Timeline.is_setup) {
|
|
Timeline.setup()
|
|
}
|
|
TickUpdates.keyframes = true;
|
|
if (outlines.children.length) {
|
|
outlines.children.empty()
|
|
Canvas.updateAllPositions()
|
|
}
|
|
if (!Animator.selected && Animator.animations.length) {
|
|
Animator.animations[0].select()
|
|
}
|
|
if (isApp && !ModelMeta.animation_path && !Animator.animations.length && ModelMeta.export_path) {
|
|
//Load
|
|
findBedrockAnimation()
|
|
}
|
|
Animator.preview()
|
|
},
|
|
leave() {
|
|
Timeline.pause()
|
|
Animator.open = false;
|
|
$('body').removeClass('animation_mode')
|
|
//resizeWindow()
|
|
//updateInterface()
|
|
Toolbars.element_origin.toPlace()
|
|
//if (quad_previews.enabled_before) {
|
|
// openQuadView()
|
|
//}
|
|
Canvas.updateAllBones()
|
|
},
|
|
showDefaultPose(no_matrix_update) {
|
|
Group.all.forEach(group => {
|
|
var bone = group.mesh;
|
|
bone.rotation.copy(bone.fix_rotation)
|
|
bone.position.copy(bone.fix_position)
|
|
bone.scale.x = bone.scale.y = bone.scale.z = 1;
|
|
|
|
if (!no_matrix_update) group.mesh.updateMatrixWorld()
|
|
})
|
|
},
|
|
preview() {
|
|
Animator.showDefaultPose(true);
|
|
|
|
Group.all.forEach(group => {
|
|
Animator.animations.forEach(animation => {
|
|
if (animation.playing) {
|
|
animation.getBoneAnimator(group).displayFrame()
|
|
}
|
|
})
|
|
group.mesh.updateMatrixWorld()
|
|
})
|
|
|
|
Animator.animations.forEach(animation => {
|
|
if (animation.playing) {
|
|
if (animation.animators.effects) {
|
|
animation.animators.effects.displayFrame();
|
|
}
|
|
}
|
|
})
|
|
|
|
if (Group.selected) {
|
|
Transformer.center()
|
|
}
|
|
Blockbench.dispatchEvent('display_animation_frame')
|
|
},
|
|
loadFile(file) {
|
|
var json = autoParseJSON(file.content)
|
|
if (json && typeof json.animations === 'object') {
|
|
for (var ani_name in json.animations) {
|
|
//Animation
|
|
var a = json.animations[ani_name]
|
|
var animation = new Animation({
|
|
name: ani_name,
|
|
loop: a.loop && (a.loop == 'hold_on_last_frame' ? 'hold' : 'loop'),
|
|
override: a.override_previous_animation,
|
|
anim_time_update: a.anim_time_update,
|
|
length: a.animation_length,
|
|
blend_weight: a.blend_weight,
|
|
//particle_effects: a.particle_effects,
|
|
//sound_effects: a.sound_effects,
|
|
}).add()
|
|
//Bones
|
|
if (a.bones) {
|
|
for (var bone_name in a.bones) {
|
|
var b = a.bones[bone_name]
|
|
bone_name = bone_name.toLowerCase();
|
|
var group = Group.all.find(group => group.name.toLowerCase() == bone_name)
|
|
if (group) {
|
|
var ba = new BoneAnimator(group.uuid, animation);
|
|
animation.animators[group.uuid] = ba;
|
|
//Channels
|
|
for (var channel in b) {
|
|
if (Animator.possible_channels[channel]) {
|
|
if (typeof b[channel] === 'string' || typeof b[channel] === 'number' || b[channel] instanceof Array) {
|
|
ba.addKeyframe({
|
|
time: 0,
|
|
channel,
|
|
values: b[channel],
|
|
})
|
|
} else if (typeof b[channel] === 'object') {
|
|
for (var timestamp in b[channel]) {
|
|
ba.addKeyframe({
|
|
time: parseFloat(timestamp),
|
|
channel,
|
|
values: b[channel][timestamp],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (a.sound_effects) {
|
|
if (!animation.animators.effects) {
|
|
animation.animators.effects = new EffectAnimator(animation);
|
|
}
|
|
for (var timestamp in a.sound_effects) {
|
|
var sound = a.sound_effects[timestamp];
|
|
if (sound instanceof Array) sound = sound[0];
|
|
animation.animators.effects.addKeyframe({
|
|
channel: 'sound',
|
|
time: parseFloat(timestamp),
|
|
effect: sound.effect,
|
|
})
|
|
}
|
|
}
|
|
if (a.particle_effects) {
|
|
if (!animation.animators.effects) {
|
|
animation.animators.effects = new EffectAnimator(animation);
|
|
}
|
|
for (var timestamp in a.particle_effects) {
|
|
var particle = a.particle_effects[timestamp];
|
|
if (particle instanceof Array) particle = particle[0];
|
|
animation.animators.effects.addKeyframe({
|
|
channel: 'particle',
|
|
time: parseFloat(timestamp),
|
|
effect: particle.effect,
|
|
locator: particle.locator,
|
|
script: particle.pre_effect_script,
|
|
})
|
|
}
|
|
}
|
|
if (a.timeline) {
|
|
if (!animation.animators.effects) {
|
|
animation.animators.effects = new EffectAnimator(animation);
|
|
}
|
|
for (var timestamp in a.timeline) {
|
|
var entry = a.timeline[timestamp];
|
|
var instructions = entry.join('\n');
|
|
animation.animators.effects.addKeyframe({
|
|
channel: 'timeline',
|
|
time: parseFloat(timestamp),
|
|
instructions
|
|
})
|
|
}
|
|
}
|
|
if (!Animator.selected) {
|
|
animation.select()
|
|
}
|
|
}
|
|
if (isApp && file.path) {
|
|
ModelMeta.animation_path = file.path
|
|
}
|
|
}
|
|
},
|
|
buildFile(options) {
|
|
if (typeof options !== 'object') {
|
|
options = false
|
|
}
|
|
var animations = {}
|
|
Animator.animations.forEach(function(a) {
|
|
var ani_tag = animations[a.name] = {};
|
|
|
|
if (a.loop == 'hold') {
|
|
ani_tag.loop = 'hold_on_last_frame';
|
|
} else if (a.loop == 'loop' || a.getMaxLength() == 0) {
|
|
ani_tag.loop = true;
|
|
}
|
|
|
|
if (a.length) ani_tag.animation_length = a.length;
|
|
if (a.override) ani_tag.override_previous_animation = true;
|
|
if (a.anim_time_update) ani_tag.anim_time_update = a.anim_time_update;
|
|
ani_tag.bones = {};
|
|
|
|
for (var uuid in a.animators) {
|
|
var animator = a.animators[uuid];
|
|
if (animator instanceof EffectAnimator) {
|
|
|
|
animator.sound.forEach(kf => {
|
|
if (!ani_tag.sound_effects) ani_tag.sound_effects = {};
|
|
let timecode = Math.clamp(trimFloatNumber(Math.round(kf.time*60)/60), 0) + '';
|
|
if (!timecode.includes('.')) {
|
|
timecode += '.0';
|
|
}
|
|
ani_tag.sound_effects[timecode] = {
|
|
effect: kf.effect
|
|
};
|
|
})
|
|
animator.particle.forEach(kf => {
|
|
if (!ani_tag.particle_effects) ani_tag.particle_effects = {};
|
|
let timecode = Math.clamp(trimFloatNumber(Math.round(kf.time*60)/60), 0) + '';
|
|
if (!timecode.includes('.')) {
|
|
timecode += '.0';
|
|
}
|
|
let script = kf.script || undefined;
|
|
if (script && !script.match(/;$/)) script += ';';
|
|
ani_tag.particle_effects[timecode] = {
|
|
effect: kf.effect,
|
|
locator: kf.locator || undefined,
|
|
pre_effect_script: kf.script || undefined
|
|
};
|
|
})
|
|
animator.timeline.forEach(kf => {
|
|
if (!ani_tag.timeline) ani_tag.timeline = {};
|
|
let timecode = Math.clamp(trimFloatNumber(Math.round(kf.time*60)/60), 0) + '';
|
|
if (!timecode.includes('.')) {
|
|
timecode += '.0';
|
|
}
|
|
ani_tag.timeline[timecode] = kf.instructions.split('\n');
|
|
})
|
|
|
|
} else if (a.animators[uuid].keyframes.length && a.animators[uuid].getGroup()) {
|
|
|
|
var group = a.animators[uuid].group;
|
|
var bone_tag = ani_tag.bones[group.name] = {};
|
|
var channels = {};
|
|
//Saving Keyframes
|
|
a.animators[uuid].keyframes.forEach(function(kf) {
|
|
if (!channels[kf.channel]) {
|
|
channels[kf.channel] = {};
|
|
}
|
|
let timecode = Math.clamp(trimFloatNumber(Math.round(kf.time*60)/60), 0) + '';
|
|
if (!timecode.includes('.')) {
|
|
timecode = timecode + '.0';
|
|
}
|
|
channels[kf.channel][timecode] = kf.getArray()
|
|
})
|
|
//Sorting keyframes
|
|
for (var channel in Animator.possible_channels) {
|
|
if (channels[channel]) {
|
|
let timecodes = Object.keys(channels[channel])
|
|
if (timecodes.length === 1) {
|
|
bone_tag[channel] = channels[channel][timecodes[0]]
|
|
if (channel == 'scale' &&
|
|
channels[channel][timecodes[0]] instanceof Array &&
|
|
channels[channel][timecodes[0]].allEqual(channels[channel][timecodes[0]][0])
|
|
) {
|
|
bone_tag[channel] = channels[channel][timecodes[0]][0];
|
|
}
|
|
} else {
|
|
timecodes.sort().forEach((time) => {
|
|
if (!bone_tag[channel]) {
|
|
bone_tag[channel] = {}
|
|
}
|
|
bone_tag[channel][time] = channels[channel][time]
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Object.keys(ani_tag.bones).length == 0) {
|
|
delete ani_tag.bones;
|
|
}
|
|
})
|
|
return {
|
|
format_version: '1.8.0',
|
|
animations: animations
|
|
}
|
|
}
|
|
}
|
|
const Timeline = {
|
|
animators: [],
|
|
selected: [],//frames
|
|
playing_sounds: [],
|
|
playback_speed: 100,
|
|
time: 0,
|
|
get second() {return Timeline.time},
|
|
get animation_length() {return Animator.selected ? Animator.selected.length : 0;},
|
|
playing: false,
|
|
selector: {
|
|
start: [0, 0],
|
|
selecting: false,
|
|
selected_before: [],
|
|
down(e) {
|
|
if (e.which !== 1 || (
|
|
!e.target.classList.contains('keyframe_section') &&
|
|
!e.target.classList.contains('animator_head_bar') &&
|
|
e.target.id !== 'timeline_body_inner'
|
|
)) {
|
|
return
|
|
};
|
|
|
|
Timeline.selector.interval = setInterval(Timeline.selector.move, 1000/60);
|
|
document.addEventListener('mouseup', Timeline.selector.end, false);
|
|
|
|
var offset = $('#timeline_body_inner').offset();
|
|
var R = Timeline.selector;
|
|
R.panel_offset = [
|
|
offset.left,
|
|
offset.top,
|
|
]
|
|
R.start = [
|
|
e.clientX - R.panel_offset[0],
|
|
e.clientY - R.panel_offset[1],
|
|
]
|
|
if (e.shiftKey) {
|
|
Timeline.selector.selected_before = Timeline.selected.slice();
|
|
}
|
|
R.selecting = true;
|
|
$('#timeline_selector').show()
|
|
Timeline.selector.move(e)
|
|
},
|
|
move(e) {
|
|
var R = Timeline.selector;
|
|
if (!R.selecting) return;
|
|
//CSS
|
|
var offset = $('#timeline_body_inner').offset();
|
|
R.panel_offset = [
|
|
offset.left,
|
|
offset.top,
|
|
]
|
|
var rect = getRectangle(R.start[0], R.start[1], mouse_pos.x - R.panel_offset[0], mouse_pos.y - R.panel_offset[1])
|
|
$('#timeline_selector')
|
|
.css('width', rect.x + 'px')
|
|
.css('height', rect.y + 'px')
|
|
.css('left', rect.ax + 'px')
|
|
.css('top', rect.ay + 'px');
|
|
//Keyframes
|
|
var epsilon = 6;
|
|
var focus = Timeline.vue._data.focus_channel;
|
|
rect.ax -= epsilon;
|
|
rect.ay -= epsilon;
|
|
rect.bx += epsilon;
|
|
rect.by += epsilon;
|
|
|
|
var min_time = (rect.ax-Timeline.vue._data.head_width-8)/Timeline.vue._data.size;
|
|
var max_time = (rect.bx-Timeline.vue._data.head_width-8)/Timeline.vue._data.size;
|
|
|
|
Timeline.selected.empty()
|
|
for (var animator of Timeline.animators) {
|
|
var node = $('#timeline_body_inner .animator[uuid=' + animator.uuid + ']').get(0)
|
|
var offset = node && node.offsetTop;
|
|
for (var kf of animator.keyframes) {
|
|
if (Timeline.selector.selected_before.includes(kf)) {
|
|
Timeline.selected.push(kf);
|
|
continue;
|
|
}
|
|
kf.selected = false;
|
|
if (kf.time > min_time &&
|
|
kf.time < max_time &&
|
|
(kf.channel == focus || !focus || focus == 'used')
|
|
) {
|
|
var channel_index = focus ? 0 : animator.channels.indexOf(kf.channel);
|
|
if (focus == 'used') {
|
|
for (var channel of animator.channels) {
|
|
if (kf.channel == channel) break;
|
|
channel_index++;
|
|
}
|
|
}
|
|
height = offset + channel_index*24 + 36;
|
|
if (height > rect.ay && height < rect.by) {
|
|
kf.selected = true;
|
|
Timeline.selected.push(kf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//Scroll body
|
|
var body = $('#timeline_body').get(0)
|
|
var body_inner = $('#timeline_body_inner').get(0)
|
|
|
|
var top = mouse_pos.y - R.panel_offset[1] - body.scrollTop;
|
|
var bot = body.scrollTop + body.clientHeight - (mouse_pos.y - R.panel_offset[1]);
|
|
var lef = mouse_pos.x - R.panel_offset[0] - body.scrollLeft - Timeline.vue._data.head_width;
|
|
var rig = body.clientWidth - (mouse_pos.x - R.panel_offset[0] - body.scrollLeft);
|
|
|
|
let speed = 15;
|
|
|
|
if (top < 0) body.scrollTop = body.scrollTop - speed;
|
|
if (bot < 0) body.scrollTop = Math.clamp(body.scrollTop + speed, 0, body_inner.clientHeight - body.clientHeight + 3);
|
|
if (lef < 0) body.scrollLeft = body.scrollLeft - speed;
|
|
if (rig < 0) body.scrollLeft = Math.clamp(body.scrollLeft + speed, 0, body_inner.clientWidth - body.clientWidth);
|
|
},
|
|
end(e) {
|
|
if (!Timeline.selector.selecting) return false;
|
|
e.stopPropagation();
|
|
document.removeEventListener('mousemove', Timeline.selector.move);
|
|
clearInterval(Timeline.selector.interval);
|
|
document.removeEventListener('mouseup', Timeline.selector.end);
|
|
|
|
updateKeyframeSelection()
|
|
Timeline.selector.selected_before.empty();
|
|
Timeline.selector.selecting = false;
|
|
$('#timeline_selector')
|
|
.css('width', 0)
|
|
.css('height', 0)
|
|
.hide()
|
|
},
|
|
},
|
|
setTime(seconds, editing) {
|
|
seconds = limitNumber(seconds, 0, 1000)
|
|
Timeline.vue._data.playhead = seconds
|
|
Timeline.time = seconds
|
|
if (!editing) {
|
|
Timeline.setTimecode(seconds)
|
|
}
|
|
if (Timeline.getMaxLength() !== Timeline.vue._data.length) {
|
|
Timeline.updateSize()
|
|
}
|
|
Timeline.revealTime(seconds)
|
|
},
|
|
revealTime(time) {
|
|
var scroll = $('#timeline_body').scrollLeft()
|
|
var playhead = time * Timeline.vue._data.size + 8
|
|
if (playhead < scroll || playhead > scroll + $('#timeline_body').width() - Timeline.vue._data.head_width) {
|
|
$('#timeline_body').scrollLeft(playhead-16)
|
|
}
|
|
},
|
|
setTimecode(time) {
|
|
let m = Math.floor(time/60)
|
|
let s = Math.floor(time%60)
|
|
let f = Math.floor((time%1) * 100)
|
|
if ((s+'').length === 1) {s = '0'+s}
|
|
if ((f+'').length === 1) {f = '0'+f}
|
|
$('#timeline_corner').text(m + ':' + s + ':' + f)
|
|
},
|
|
snapTime(time) {
|
|
//return time;
|
|
if (time == undefined || isNaN(time)) {
|
|
time = Timeline.time;
|
|
}
|
|
var fps = Math.clamp(settings.animation_snap.value, 1, 120);
|
|
return Math.clamp(Math.round(time*fps)/fps, 0);
|
|
},
|
|
getStep() {
|
|
return 1/Math.clamp(settings.animation_snap.value, 1, 120);
|
|
},
|
|
setup() {
|
|
$('#timeline_body').mousedown(Timeline.selector.down)
|
|
|
|
$('#timeline_time').on('mousedown touchstart', e => {
|
|
if (e.which !== 1 && !event.changedTouches) return;
|
|
if (e.target.classList.contains('timeline_marker')) return;
|
|
|
|
if (e.target.id == 'timeline_endbracket') {
|
|
|
|
if (Animator.selected) {
|
|
Timeline.dragging_endbracket = true;
|
|
Undo.initEdit({animations: [Animator.selected]});
|
|
} else {
|
|
Blockbench.showQuickMessage('message.no_animation_selected');
|
|
}
|
|
|
|
} else {
|
|
|
|
convertTouchEvent(e);
|
|
Timeline.dragging_playhead = true;
|
|
|
|
let offset = e.clientX - $('#timeline_time').offset().left;
|
|
let time = Timeline.snapTime(offset / Timeline.vue._data.size);
|
|
Timeline.setTime(time);
|
|
Animator.preview();
|
|
}
|
|
})
|
|
$(document).on('mousemove touchmove', e => {
|
|
if (Timeline.dragging_playhead) {
|
|
|
|
convertTouchEvent(e);
|
|
let offset = e.clientX - $('#timeline_time').offset().left;
|
|
let time = Timeline.snapTime(offset / Timeline.vue._data.size)
|
|
if (Timeline.time != time) {
|
|
Timeline.setTime(time)
|
|
Animator.preview()
|
|
}
|
|
} else if (Timeline.dragging_endbracket) {
|
|
|
|
convertTouchEvent(e);
|
|
let offset = e.clientX - $('#timeline_time').offset().left;
|
|
let time = Timeline.snapTime(offset / Timeline.vue._data.size)
|
|
|
|
Animator.selected.setLength(time)
|
|
Timeline.revealTime(time)
|
|
|
|
}
|
|
})
|
|
.on('mouseup touchend', e => {
|
|
if (Timeline.dragging_playhead) {
|
|
delete Timeline.dragging_playhead
|
|
Timeline.pause();
|
|
|
|
} else if (Timeline.dragging_endbracket) {
|
|
|
|
Undo.finishEdit('Change Animation Length')
|
|
delete Timeline.dragging_endbracket
|
|
}
|
|
})
|
|
//Keyframe inputs
|
|
$('.keyframe_input').click(e => {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
}).focusout(e => {
|
|
Undo.finishEdit('edit keyframe')
|
|
})
|
|
//Enter Time
|
|
$('#timeline_corner').click(e => {
|
|
if ($('#timeline_corner').attr('contenteditable') == 'true') return;
|
|
|
|
$('#timeline_corner').attr('contenteditable', true).focus().select()
|
|
var times = $('#timeline_corner').text().split(':')
|
|
while (times.length < 3) {
|
|
times.push('00')
|
|
}
|
|
var node = $('#timeline_corner').get(0).childNodes[0]
|
|
var selection = window.getSelection();
|
|
var range = document.createRange();
|
|
|
|
var sel = [0, node.length]
|
|
if (e.offsetX < 24) {
|
|
sel = [0, times[0].length]
|
|
} else if (e.offsetX < 54) {
|
|
sel = [times[0].length+1, times[1].length]
|
|
} else if (e.offsetX < 80) {
|
|
sel = [times[0].length+times[1].length+2, times[2].length]
|
|
}
|
|
sel[1] = limitNumber(sel[0]+sel[1], sel[0], node.length)
|
|
|
|
range.setStart(node, sel[0])
|
|
range.setEnd(node, sel[1])
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
})
|
|
.on('focusout keydown', e => {
|
|
if (e.type === 'focusout' || Keybinds.extra.confirm.keybind.isTriggered(e) || Keybinds.extra.cancel.keybind.isTriggered(e)) {
|
|
$('#timeline_corner').attr('contenteditable', false)
|
|
Timeline.setTimecode(Timeline.time)
|
|
}
|
|
})
|
|
.on('keyup', e => {
|
|
var times = $('#timeline_corner').text().split(':')
|
|
times.forEach((t, i) => {
|
|
times[i] = parseInt(t)
|
|
if (isNaN(times[i])) {
|
|
times[i] = 0
|
|
}
|
|
})
|
|
while (times.length < 3) {
|
|
times.push(0)
|
|
}
|
|
var seconds
|
|
= times[0]*60
|
|
+ limitNumber(times[1], 0, 59)
|
|
+ limitNumber(times[2]/100, 0, 99)
|
|
if (Math.abs(seconds-Timeline.time) > 1e-3 ) {
|
|
Timeline.setTime(seconds, true)
|
|
Animator.preview()
|
|
}
|
|
})
|
|
|
|
$('#timeline_vue').on('mousewheel scroll', function(e) {
|
|
e.preventDefault()
|
|
var body = $('#timeline_body').get(0)
|
|
if (event.shiftKey) {
|
|
body.scrollLeft += event.deltaY/4
|
|
|
|
} else if (event.ctrlOrCmd) {
|
|
|
|
let mouse_pos = event.clientX - $(this).offset().left;
|
|
|
|
var zoom = 1 - event.deltaY/600
|
|
let original_size = Timeline.vue._data.size
|
|
Timeline.vue._data.size = limitNumber(Timeline.vue._data.size * zoom, 10, 1000)
|
|
//let val = ((body.scrollLeft + mouse_pos) * (Timeline.vue._data.size - original_size) ) / 128
|
|
|
|
let size_ratio = Timeline.vue._data.size / original_size
|
|
let offset = mouse_pos - body.scrollLeft - 180
|
|
let val = (size_ratio-1) * offset;
|
|
// todo: optimize zooming in
|
|
body.scrollLeft += val
|
|
/*
|
|
Timeline.vue._data.size = limitNumber(Timeline.vue._data.size * zoom, 10, 1000)
|
|
body.scrollLeft *= zoom
|
|
let l = (event.offsetX / body.clientWidth) * 500 * (event.deltaY<0?1:-0.2)
|
|
body.scrollLeft += l
|
|
*/
|
|
|
|
} else {
|
|
body.scrollTop += event.deltaY/6.25
|
|
}
|
|
Timeline.updateSize()
|
|
event.preventDefault();
|
|
});
|
|
$('#timeline_body').on('scroll', e => {
|
|
Timeline.vue._data.scroll_left = $('#timeline_body').scrollLeft()||0;
|
|
})
|
|
|
|
BarItems.slider_animation_speed.update()
|
|
Timeline.is_setup = true
|
|
Timeline.setTime(0)
|
|
},
|
|
update() {
|
|
//Draggable
|
|
$('#timeline_body .keyframe:not(.ui-draggable)').draggable({
|
|
axis: 'x',
|
|
distance: 4,
|
|
helper: () => $('<div></div>'),
|
|
start: function(event, ui) {
|
|
|
|
var id = $(event.target).attr('id');
|
|
var clicked = Timeline.keyframes.findInArray('uuid', id)
|
|
|
|
if (!$(event.target).hasClass('selected') && !event.shiftKey && Timeline.selected.length != 0) {
|
|
clicked.select()
|
|
} else if (clicked && !clicked.selected) {
|
|
clicked.select({shiftKey: true})
|
|
}
|
|
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
Timeline.dragging_keyframes = true;
|
|
|
|
var i = 0;
|
|
for (var kf of Timeline.selected) {
|
|
kf.time_before = kf.time
|
|
}
|
|
},
|
|
drag: function(event, ui) {
|
|
var difference = (ui.position.left - ui.originalPosition.left - 8) / Timeline.vue._data.size;
|
|
var id = $(ui.helper).attr('id')
|
|
|
|
for (var kf of Timeline.selected) {
|
|
var t = limitNumber(kf.time_before + difference, 0, 256)
|
|
if (kf.uuid === id) {
|
|
ui.position.left = t * Timeline.vue._data.size + 8
|
|
}
|
|
kf.time = Timeline.snapTime(t);
|
|
}
|
|
BarItems.slider_keyframe_time.update()
|
|
Animator.preview()
|
|
},
|
|
stop: function(event, ui) {
|
|
var deleted = []
|
|
for (var kf of Timeline.selected) {
|
|
delete kf.time_before;
|
|
kf.replaceOthers(deleted);
|
|
}
|
|
Undo.addKeyframeCasualties(deleted);
|
|
Undo.finishEdit('drag keyframes')
|
|
setTimeout(() => {
|
|
Timeline.dragging_keyframes = false;
|
|
}, 20)
|
|
}
|
|
})
|
|
},
|
|
getMaxLength() {
|
|
var max_length = ($('#timeline_body').width()-8) / Timeline.vue._data.size;
|
|
Timeline.keyframes.forEach((kf) => {
|
|
max_length = Math.max(max_length, kf.time)
|
|
})
|
|
max_length = Math.max(max_length, Timeline.time) + 50/Timeline.vue._data.size
|
|
return max_length;
|
|
},
|
|
updateSize() {
|
|
let size = Timeline.vue._data.size
|
|
Timeline.vue._data.length = Timeline.getMaxLength()
|
|
Timeline.vue._data.timecodes.empty()
|
|
|
|
var step = 1
|
|
if (size < 1) {step = 1}
|
|
else if (size < 20) {step = 4}
|
|
else if (size < 40) {step = 2}
|
|
else if (size < 100) {step = 1}
|
|
else if (size < 256) {step = 0.5}
|
|
else if (size < 520) {step = 0.25}
|
|
else if (size < 660) {step = 0.2}
|
|
else if (size < 860) {step = 0.1}
|
|
else {step = 0.05}
|
|
|
|
|
|
if (step < 1) {
|
|
var FPS = Timeline.getStep();
|
|
step = Math.round(step/FPS) * FPS
|
|
//step = 1/Math.round(1/step)
|
|
}
|
|
|
|
let substeps = step / Timeline.getStep()
|
|
while (substeps > 8) {
|
|
substeps /= 2;
|
|
}
|
|
//substeps = Math.round(substeps)
|
|
|
|
|
|
var i = 0;
|
|
while (i < Timeline.vue._data.length) {
|
|
Timeline.vue._data.timecodes.push({
|
|
time: i,
|
|
width: step,
|
|
substeps: substeps,
|
|
text: Math.round(i*100)/100
|
|
})
|
|
i += step;
|
|
}
|
|
},
|
|
updateScroll(e) {
|
|
$('.channel_head').css('left', scroll_amount+'px')
|
|
$('#timeline_time').css('left', -scroll_amount+'px')
|
|
},
|
|
unselect(e) {
|
|
if (!Animator.selected) return;
|
|
Timeline.keyframes.forEach((kf) => {
|
|
if (kf.selected) {
|
|
Timeline.selected.remove(kf)
|
|
}
|
|
kf.selected = false
|
|
})
|
|
TickUpdates.keyframe_selection = true;
|
|
},
|
|
start() {
|
|
if (!Animator.selected) return;
|
|
Animator.selected.getMaxLength()
|
|
Timeline.pause()
|
|
Timeline.playing = true
|
|
BarItems.play_animation.setIcon('pause')
|
|
Timeline.interval = setInterval(Timeline.loop, 100/6)
|
|
Timeline.loop()
|
|
},
|
|
loop() {
|
|
Animator.preview()
|
|
if (Animator.selected && Timeline.time < (Animator.selected.length||1e3)) {
|
|
|
|
Timeline.setTime(Timeline.time + (1/60) * (Timeline.playback_speed/100))
|
|
} else {
|
|
if (Animator.selected.loop == 'once') {
|
|
Timeline.setTime(0)
|
|
Timeline.pause()
|
|
Animator.preview()
|
|
} else if (Animator.selected.loop == 'hold') {
|
|
Timeline.pause()
|
|
} else {
|
|
Timeline.setTime(0)
|
|
Timeline.start()
|
|
}
|
|
}
|
|
},
|
|
pause() {
|
|
Timeline.playing = false;
|
|
BarItems.play_animation.setIcon('play_arrow')
|
|
if (Timeline.interval) {
|
|
clearInterval(Timeline.interval)
|
|
Timeline.interval = false
|
|
}
|
|
Timeline.playing_sounds.forEach(media => {
|
|
if (!media.paused) {
|
|
media.pause();
|
|
}
|
|
})
|
|
Timeline.playing_sounds.empty();
|
|
},
|
|
get keyframes() {
|
|
var keyframes = [];
|
|
Timeline.animators.forEach(animator => {
|
|
keyframes = [...keyframes, ...animator.keyframes]
|
|
})
|
|
return keyframes;
|
|
},
|
|
showMenu(event) {
|
|
if (event.target.nodeName == 'KEYFRAME' || event.target.parentElement.nodeName == 'KEYFRAME') return;
|
|
Timeline.menu.open(event, event);
|
|
},
|
|
menu: new Menu([
|
|
'paste',
|
|
'_',
|
|
'select_all',
|
|
'bring_up_all_animations',
|
|
'fold_all_animations',
|
|
'clear_timeline',
|
|
])
|
|
}
|
|
Molang.global_variables = {
|
|
'true': 1,
|
|
'false': 0,
|
|
get 'query.anim_time'() {
|
|
return Timeline.time;
|
|
},
|
|
get 'query.life_time'() {
|
|
return Timeline.time;
|
|
},
|
|
get 'time'() {
|
|
return Timeline.time;
|
|
}
|
|
}
|
|
Molang.variableHandler = function (variable) {
|
|
var inputs = $('#var_placeholder_area').val().split('\n')
|
|
var i = 0;
|
|
while (i < inputs.length) {
|
|
let key, val;
|
|
[key, val] = inputs[i].replace(/[\s;]/g, '').split('=')
|
|
if (key === variable) {
|
|
return Molang.parse(val)
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
onVueSetup(function() {
|
|
Animator.vue = new Vue({
|
|
el: '#animations_list',
|
|
data: {
|
|
animations: Animator.animations
|
|
},
|
|
methods: {
|
|
sort(event) {
|
|
var item = Animator.animations.splice(event.oldIndex, 1)[0];
|
|
Animator.animations.splice(event.newIndex, 0, item);
|
|
}
|
|
}
|
|
})
|
|
Timeline.vue = new Vue({
|
|
el: '#timeline_vue',
|
|
data: {
|
|
size: 150,
|
|
length: 10,
|
|
animation_length: 0,
|
|
scroll_left: 0,
|
|
head_width: 180,
|
|
timecodes: [],
|
|
animators: Timeline.animators,
|
|
markers: [],
|
|
focus_channel: null,
|
|
playhead: Timeline.time
|
|
},
|
|
methods: {
|
|
toggleAnimator(animator) {
|
|
animator.expanded = !animator.expanded;
|
|
},
|
|
removeAnimator(animator) {
|
|
Timeline.animators.remove(animator);
|
|
}
|
|
}
|
|
})
|
|
})
|
|
Blockbench.addDragHandler('animation', {
|
|
extensions: ['animation.json'],
|
|
readtype: 'text',
|
|
condition: {modes: ['animate']},
|
|
}, function(files) {
|
|
Animator.loadFile(files[0])
|
|
})
|
|
|
|
BARS.defineActions(function() {
|
|
new Action('add_animation', {
|
|
icon: 'fa-plus-circle',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
var animation = new Animation({
|
|
name: 'animation.' + (Project.geometry_name||'model') + '.new'
|
|
}).add(true).rename()
|
|
|
|
}
|
|
})
|
|
new Action('load_animation_file', {
|
|
icon: 'fa-file-video',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
var path = ModelMeta.export_path
|
|
if (isApp) {
|
|
var exp = new RegExp(osfs.replace('\\', '\\\\')+'models'+osfs.replace('\\', '\\\\'))
|
|
var m_index = path.search(exp)
|
|
if (m_index > 3) {
|
|
path = path.substr(0, m_index) + osfs + 'animations' + osfs + pathToName(ModelMeta.export_path).replace(/\.geo/, '.animation')
|
|
}
|
|
}
|
|
Blockbench.import({
|
|
resource_id: 'animation',
|
|
extensions: ['json'],
|
|
type: 'JSON Animation',
|
|
startpath: path
|
|
}, function(files) {
|
|
Animator.loadFile(files[0])
|
|
})
|
|
}
|
|
})
|
|
new Action('export_animation_file', {
|
|
icon: 'save',
|
|
category: 'animation',
|
|
click: function () {
|
|
var content = autoStringify(Animator.buildFile())
|
|
var path = ModelMeta.animation_path
|
|
|
|
if (isApp && !path) {
|
|
path = ModelMeta.export_path
|
|
var exp = new RegExp(osfs.replace('\\', '\\\\')+'models'+osfs.replace('\\', '\\\\'))
|
|
var m_index = path.search(exp)
|
|
if (m_index > 3) {
|
|
path = path.substr(0, m_index) + osfs + 'animations' + osfs + pathToName(ModelMeta.export_path, true)
|
|
}
|
|
if (path.match(/\.geo\.json$/)) {
|
|
path = path.replace(/\.geo\.json$/, '.animation.json')
|
|
} else {
|
|
path = path.replace(/\.json$/, '.animation.json')
|
|
}
|
|
}
|
|
Blockbench.export({
|
|
resource_id: 'animation',
|
|
type: 'JSON Animation',
|
|
extensions: ['json'],
|
|
name: (Project.geometry_name||'model')+'.animation',
|
|
startpath: path,
|
|
content: content,
|
|
}, (real_path) => {
|
|
ModelMeta.animation_path = real_path;
|
|
})
|
|
|
|
}
|
|
})
|
|
new Action('play_animation', {
|
|
icon: 'play_arrow',
|
|
category: 'animation',
|
|
keybind: new Keybind({key: 32}),
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
|
|
if (!Animator.selected) {
|
|
Blockbench.showQuickMessage('message.no_animation_selected')
|
|
return;
|
|
}
|
|
if (Timeline.playing) {
|
|
Timeline.pause()
|
|
} else {
|
|
Timeline.start()
|
|
}
|
|
}
|
|
})
|
|
new NumSlider('slider_animation_length', {
|
|
category: 'animation',
|
|
condition: () => Animator.open && Animator.selected,
|
|
getInterval(event) {
|
|
if (event && event.shiftKey) return 1;
|
|
return 1/Math.clamp(settings.animation_snap.value, 1, 120)
|
|
},
|
|
get: function() {
|
|
return Animator.selected.length
|
|
},
|
|
change: function(modify) {
|
|
Animator.selected.setLength(limitNumber(modify(Animator.selected.length), 0, 1e4))
|
|
},
|
|
onBefore: function() {
|
|
Undo.initEdit({animations: [Animator.selected]});
|
|
},
|
|
onAfter: function() {
|
|
Undo.finishEdit('Change Animation Length')
|
|
}
|
|
})
|
|
new NumSlider('slider_animation_speed', {
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
get: function() {
|
|
return Timeline.playback_speed;
|
|
},
|
|
change: function(modify) {
|
|
Timeline.playback_speed = limitNumber(modify(Timeline.playback_speed), 0, 10000)
|
|
},
|
|
getInterval: (e) => {
|
|
var val = BarItems.slider_animation_speed.get()
|
|
if (e.shiftKey) {
|
|
if (val < 50) {
|
|
return 10;
|
|
} else {
|
|
return 50;
|
|
}
|
|
}
|
|
if (e.ctrlOrCmd) {
|
|
if (val < 500) {
|
|
return 1;
|
|
} else {
|
|
return 10;
|
|
}
|
|
}
|
|
if (val < 10) {
|
|
return 1;
|
|
} else if (val < 50) {
|
|
return 5;
|
|
} else if (val < 160) {
|
|
return 10;
|
|
} else if (val < 300) {
|
|
return 20;
|
|
} else if (val < 1000) {
|
|
return 50;
|
|
} else {
|
|
return 500;
|
|
}
|
|
}
|
|
})
|
|
new NumSlider('slider_keyframe_time', {
|
|
category: 'animation',
|
|
condition: () => Animator.open && Timeline.selected.length,
|
|
getInterval(event) {
|
|
if (event && event.shiftKey) return 1;
|
|
return 1/Math.clamp(settings.animation_snap.value, 1, 120)
|
|
},
|
|
get: function() {
|
|
return Timeline.selected[0] ? Timeline.selected[0].time : 0
|
|
},
|
|
change: function(modify) {
|
|
Timeline.selected.forEach((kf) => {
|
|
kf.time = Timeline.snapTime(limitNumber(modify(kf.time), 0, 1e4))
|
|
})
|
|
Animator.preview()
|
|
},
|
|
onBefore: function() {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
},
|
|
onAfter: function() {
|
|
Undo.finishEdit('move keyframes')
|
|
}
|
|
})
|
|
new Action('reset_keyframe', {
|
|
icon: 'replay',
|
|
category: 'animation',
|
|
condition: () => Animator.open && Timeline.selected.length,
|
|
click: function () {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
Timeline.selected.forEach((kf) => {
|
|
var n = kf.channel === 'scale' ? '1' : '0';
|
|
kf.extend({
|
|
x: n,
|
|
y: n,
|
|
z: n,
|
|
w: kf.isQuaternion ? 0 : undefined
|
|
})
|
|
})
|
|
Undo.finishEdit('reset keyframes')
|
|
updateKeyframeSelection()
|
|
Animator.preview()
|
|
}
|
|
})
|
|
new Action('resolve_keyframe_expressions', {
|
|
icon: 'functions',
|
|
category: 'animation',
|
|
condition: () => Animator.open && Timeline.selected.length,
|
|
click: function () {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
let time_before = Timeline.time;
|
|
Timeline.selected.forEach((kf) => {
|
|
if (kf.animator.fillValues) {
|
|
Timeline.time = kf.time;
|
|
kf.animator.fillValues(kf, null, false);
|
|
}
|
|
})
|
|
Timeline.time = time_before;
|
|
Undo.finishEdit('reset keyframes')
|
|
updateKeyframeSelection()
|
|
}
|
|
})
|
|
new Action('change_keyframe_file', {
|
|
icon: 'fa-file-audio',
|
|
category: 'animation',
|
|
condition: () => (Animator.open && Timeline.selected.length && Timeline.selected[0].channel == 'sound' && isApp),
|
|
click: function () {
|
|
Blockbench.import({
|
|
resource_id: 'animation_audio',
|
|
extensions: ['ogg'],
|
|
type: 'Audio File',
|
|
startpath: Timeline.selected[0].file
|
|
}, function(files) {
|
|
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
Timeline.selected.forEach((kf) => {
|
|
if (kf.channel == 'sound') {
|
|
kf.file = files[0].path;
|
|
}
|
|
})
|
|
Undo.finishEdit('changed keyframe audio file')
|
|
})
|
|
}
|
|
})
|
|
new Action('reverse_keyframes', {
|
|
icon: 'swap_horizontal_circle',
|
|
category: 'animation',
|
|
condition: () => Animator.open && Timeline.selected.length,
|
|
click: function () {
|
|
var start = 1e9;
|
|
var end = 0;
|
|
Timeline.selected.forEach((kf) => {
|
|
start = Math.min(start, kf.time);
|
|
end = Math.max(end, kf.time);
|
|
})
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
Timeline.selected.forEach((kf) => {
|
|
kf.time = end + start - kf.time;
|
|
})
|
|
Undo.finishEdit('reverse keyframes')
|
|
updateKeyframeSelection()
|
|
Animator.preview()
|
|
}
|
|
})
|
|
|
|
new Action('previous_keyframe', {
|
|
icon: 'fa-arrow-circle-left',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
|
|
var time = Timeline.time;
|
|
function getDelta(kf, abs) {
|
|
return kf.time - time
|
|
}
|
|
let animator = Timeline.selected_animator
|
|
|| (Timeline.selected[0] && Timeline.selected[0].animator)
|
|
|| Timeline.animators[0];
|
|
let channel = Timeline.selected[0] ? Timeline.selected[0].channel : (animator && animator.channels[0]);
|
|
|
|
var matches = []
|
|
for (var kf of Timeline.keyframes) {
|
|
if ((!animator || animator == kf.animator) && (!channel || channel == kf.channel)) {
|
|
let delta = getDelta(kf)
|
|
if (delta < 0) {
|
|
matches.push(kf)
|
|
}
|
|
}
|
|
}
|
|
matches.sort((a, b) => {
|
|
return Math.abs(getDelta(a)) - Math.abs(getDelta(b))
|
|
})
|
|
var kf = matches[0]
|
|
if (kf) {
|
|
kf.select().callPlayhead()
|
|
} else {
|
|
if (Timeline.selected.length) {
|
|
selectAllKeyframes()
|
|
selectAllKeyframes()
|
|
}
|
|
Timeline.setTime(0)
|
|
Animator.preview()
|
|
}
|
|
}
|
|
})
|
|
new Action('next_keyframe', {
|
|
icon: 'fa-arrow-circle-right',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
|
|
var time = Timeline.time;
|
|
function getDelta(kf, abs) {
|
|
return kf.time - time
|
|
}
|
|
let animator = Timeline.selected_animator
|
|
|| (Timeline.selected[0] && Timeline.selected[0].animator)
|
|
|| Timeline.animators[0];
|
|
let channel = Timeline.selected[0] ? Timeline.selected[0].channel : (animator && animator.channels[0]);
|
|
|
|
var matches = []
|
|
for (var kf of Timeline.keyframes) {
|
|
if ((!animator || animator == kf.animator) && (!channel || channel == kf.channel)) {
|
|
let delta = getDelta(kf)
|
|
if (delta > 0) {
|
|
matches.push(kf)
|
|
}
|
|
}
|
|
}
|
|
matches.sort((a, b) => {
|
|
return Math.abs(getDelta(a)) - Math.abs(getDelta(b))
|
|
})
|
|
var kf = matches[0]
|
|
if (kf) {
|
|
kf.select().callPlayhead()
|
|
}
|
|
}
|
|
})
|
|
|
|
new Action('jump_to_timeline_start', {
|
|
icon: 'skip_previous',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
keybind: new Keybind({key: 36}),
|
|
click: function () {
|
|
Timeline.setTime(0)
|
|
Animator.preview()
|
|
}
|
|
})
|
|
|
|
new Action('jump_to_timeline_end', {
|
|
icon: 'skip_next',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
keybind: new Keybind({key: 35}),
|
|
click: function () {
|
|
Timeline.setTime(Animator.selected ? Animator.selected.length : 0)
|
|
Animator.preview()
|
|
}
|
|
})
|
|
|
|
new Action('move_keyframe_back', {
|
|
icon: 'arrow_back',
|
|
category: 'transform',
|
|
condition: {modes: ['animate'], method: () => (!open_menu && Timeline.selected.length)},
|
|
keybind: new Keybind({key: 37}),
|
|
click: function (e) {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
Timeline.selected.forEach((kf) => {
|
|
kf.time = Timeline.snapTime(limitNumber(kf.time - Timeline.getStep(), 0, 1e4))
|
|
})
|
|
Animator.preview()
|
|
Undo.finishEdit('move keyframes')
|
|
}
|
|
})
|
|
new Action('move_keyframe_forth', {
|
|
icon: 'arrow_forward',
|
|
category: 'transform',
|
|
condition: {modes: ['animate'], method: () => (!open_menu && Timeline.selected.length)},
|
|
keybind: new Keybind({key: 39}),
|
|
click: function (e) {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
Timeline.selected.forEach((kf) => {
|
|
kf.time = Timeline.snapTime(limitNumber(kf.time + Timeline.getStep(), 0, 1e4))
|
|
})
|
|
Animator.preview()
|
|
Undo.finishEdit('move keyframes')
|
|
}
|
|
})
|
|
|
|
new Action('add_keyframe', {
|
|
icon: 'add_circle',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
keybind: new Keybind({key: 81, shift: null}),
|
|
click: function (event) {
|
|
var animator = Timeline.selected_animator;
|
|
if (!animator) return;
|
|
var channel = animator.channels[0];
|
|
if (Toolbox.selected.id == 'rotate_tool' && animator.channels.includes('rotation')) channel = 'rotation';
|
|
if (Toolbox.selected.id == 'move_tool' && animator.channels.includes('position')) channel = 'position';
|
|
if (Toolbox.selected.id == 'resize_tool' && animator.channels.includes('scale')) channel = 'scale';
|
|
animator.createKeyframe((event && event.shiftKey) ? {} : null, Timeline.time, channel, true);
|
|
if (event && event.shiftKey) {
|
|
Animator.preview();
|
|
}
|
|
}
|
|
})
|
|
|
|
new Action('add_marker', {
|
|
icon: 'flag',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
keybind: new Keybind({ctrl: true, key: 77}),
|
|
click: function (event) {
|
|
if (!Animator.selected) {
|
|
Blockbench.showQuickMessage('message.no_animation_selected')
|
|
return;
|
|
}
|
|
var time = Timeline.snapTime();
|
|
var original_marker;
|
|
for (var m of Animator.selected.markers) {
|
|
if (Math.abs(m.time - time) < 0.01) {
|
|
original_marker = m;
|
|
break;
|
|
}
|
|
}
|
|
if (original_marker) {
|
|
Animator.selected.markers.remove(original_marker);
|
|
} else {
|
|
let marker = new TimelineMarker({time});
|
|
Animator.selected.markers.push(marker);
|
|
}
|
|
}
|
|
})
|
|
|
|
new Action('bring_up_all_animations', {
|
|
icon: 'fa-sort-amount-up',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
if (!Animator.selected) return;
|
|
for (var uuid in Animator.selected.animators) {
|
|
var ba = Animator.selected.animators[uuid]
|
|
if (ba && ba.keyframes.length) {
|
|
ba.addToTimeline();
|
|
}
|
|
}
|
|
|
|
}
|
|
})
|
|
new Action('fold_all_animations', {
|
|
icon: 'format_indent_decrease',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
for (var animator of Timeline.animators) {
|
|
animator.expanded = false;
|
|
}
|
|
|
|
}
|
|
})
|
|
new Action('clear_timeline', {
|
|
icon: 'clear_all',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
Timeline.vue._data.animators.purge();
|
|
unselectAll();
|
|
}
|
|
})
|
|
new Action('select_effect_animator', {
|
|
icon: 'fa-magic',
|
|
category: 'animation',
|
|
condition: {modes: ['animate']},
|
|
click: function () {
|
|
if (!Animator.selected) return;
|
|
if (!Animator.selected.animators.effects) {
|
|
var ea = Animator.selected.animators.effects = new EffectAnimator(Animator.selected);
|
|
}
|
|
Animator.selected.animators.effects.select()
|
|
}
|
|
})
|
|
new BarSelect('timeline_focus', {
|
|
options: {
|
|
all: true,
|
|
used: true,
|
|
rotation: tl('timeline.rotation'),
|
|
position: tl('timeline.position'),
|
|
scale: tl('timeline.scale'),
|
|
},
|
|
onChange: function(slider) {
|
|
var val = slider.get();
|
|
Timeline.vue._data.focus_channel = val != 'all' ? val : null;
|
|
}
|
|
})
|
|
/*
|
|
//Inverse Kinematics
|
|
new Action('ik_enabled', {
|
|
icon: 'check_box_outline_blank',
|
|
category: 'animation',
|
|
click: function () {
|
|
Group.selected.ik_enabled = !Group.selected.ik_enabled;
|
|
updateNslideValues();
|
|
Transformer.updateSelection();
|
|
}
|
|
})
|
|
new NumSlider('slider_ik_chain_length', {
|
|
category: 'animation',
|
|
condition: () => Animator.open && Group.selected,
|
|
get: function() {
|
|
return Group.selected.ik_chain_length||0;
|
|
},
|
|
settings: {
|
|
min: 0, max: 64, default: 0,
|
|
interval: function(event) {
|
|
return 1;
|
|
}
|
|
},
|
|
change: function(modify) {
|
|
Group.selected.ik_chain_length = modify(Group.selected.ik_chain_length);
|
|
},
|
|
onBefore: function() {
|
|
Undo.initEdit({keyframes: Timeline.selected})
|
|
},
|
|
onAfter: function() {
|
|
Undo.finishEdit('move keyframes')
|
|
}
|
|
})*/
|
|
|
|
})
|