class Animation { constructor(data) { this.name = ''; this.uuid = guid() this.loop = true; this.override = false; this.selected = false; this.anim_time_update = ''; this.length = 0 this.bones = { //uuid: BoneAnimator } if (typeof data === 'object') { this.extend(data) } } extend(data) { Merge.string(this, data, 'name') Merge.boolean(this, data, 'loop') Merge.boolean(this, data, 'override') Merge.string(this, data, 'anim_time_update') Merge.number(this, data, 'length') return this; } undoCopy() { 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 (this.bones.length) { copy.bones = {} for (var uuid in this.bones) { var kfs = this.bones[uuid].keyframes if (kfs && kfs.length) { var kfs_copy = copy.bones[uuid] = [] kfs.forEach(kf => { kfs_copy.push(kf.undoCopy()) }) } } } return copy; } select() { var scope = this; var selected_bone = selected_group Animator.animations.forEach(function(a) { a.selected = false; }) Prop.active_panel = 'animations' this.selected = true Animator.selected = this unselectAll() BarItems.slider_animation_length.update() function iterate(arr) { arr.forEach((it) => { if (it.type === 'group') { Animator.selected.getBoneAnimator(it) if (it.children && it.children.length) { iterate(it.children) } } }) } iterate(TreeElements) if (selected_bone) { selected_bone.select() } Animator.preview() return this; } rename() { var scope = this; Blockbench.textPrompt('message.rename_animation', this.name, function(name) { if (name) { scope.name = name } }) return this; } editUpdateVariable() { var scope = this; Blockbench.textPrompt('message.animation_update_var', this.anim_time_update, function(name) { if (name) { scope.anim_time_update = name } }) return this; } showContextMenu(event) { this.select(); this.menu.open(event, this); return this; } getBoneAnimator(group) { if (!group && selected_group) { group = selected_group } else if (!group) { return; } var uuid = group.uuid if (!this.bones[uuid]) { var ba = this.bones[uuid] = new BoneAnimator() ba.uuid = uuid } return this.bones[uuid]; } displayFrame(time) { for (var uuid in this.bones) { this.bones[uuid].displayFrame(time) } if (selected_group) { centerTransformer() } Blockbench.dispatchEvent('display_animation_frame') } add() { if (!Animator.animations.includes(this)) { Animator.animations.push(this) } return this; } remove() { if (Animator.selected === this) { Animator.selected = false } Animator.animations.remove(this) Blockbench.dispatchEvent('remove_animation', {animation: this}) return this; } getMaxLength() { var len = this.length||0 for (var uuid in this.bones) { var bone = this.bones[uuid] var i = 0; while (i < bone.keyframes.length) { len = Math.max(len, bone.keyframes[i].time) i++; } } this.length = len if (this == Animator.selected) { BarItems.slider_animation_length.update() } return len } } Animation.prototype.menu = new Menu([ {name: 'generic.rename', icon: 'text_format', click: function(animation) { animation.rename() }}, {name: 'menu.animation.loop', icon: (a) => (a.loop?'check_box':'check_box_outline_blank'), click: function(animation) { animation.loop = !animation.loop }}, {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() }}, {name: 'generic.delete', icon: 'delete', click: function(animation) { animation.remove() }}, /* rename Loop: checkbox Override: checkbox anim_time_update: WalkPosition delete */ ]) class BoneAnimator { constructor() { this.keyframes = [] this.uuid; } getGroup() { this.group = TreeElements.findRecursive('uuid', this.uuid) if (!this.group) { console.log('no group found for '+this.uuid) } return this.group } addKeyframe(values, time, channel) { var keyframe = new Keyframe({ time: time, channel: channel }) if (values && typeof values === 'object') { 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 }) } else { var ref = this.interpolate(time, channel, true) 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 }) } } this.keyframes.push(keyframe) keyframe.parent = this; return keyframe; } pushKeyframe(keyframe) { this.keyframes.push(keyframe) keyframe.parent = 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 bone.rotation.copy(bone.fix_rotation) 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.PI / (180 / n) * (i == 2 ? 1 : -1) }) } return this; } displayPosition(arr) { var bone = this.group.mesh bone.position.copy(bone.fix_position) if (arr) { bone.position.add(new THREE.Vector3().fromArray(arr)) } return this; } displayScale(arr) { var bone = this.group.mesh if (arr) { bone.scale.x = bone.scale.y = bone.scale.z = arr[0] ? arr[0] : 0.00001 } else { bone.scale.x = bone.scale.y = bone.scale.z = 1 } return this; } interpolate(time, channel, allow_expression) { var i = 0; var before = false var after = false var result = false while (i < this.keyframes.length) { var keyframe = this.keyframes[i] if (keyframe.channel !== channel) { } else 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) result = [ before.getLerp(after, 'x', alpha, allow_expression) ] if (before.channel !== 'scale') { result[1] = before.getLerp(after, 'y', alpha, allow_expression) result[2] = 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') ] if (keyframe.channel !== 'scale') { result[1] = keyframe[method]('y') result[2] = keyframe[method]('z') } if (keyframe.isQuaternion) { result[3] = keyframe[method]('w') } } return result } displayFrame(time) { if (!this.doRender()) return; this.getGroup() for (var channel in Animator.possible_channels) { var result = this.interpolate(time, channel) if (channel === 'rotation') { this.displayRotation(result) } else if (channel === 'position') { this.displayPosition(result) } else if (channel === 'scale') { this.displayScale(result) } } this.group.mesh.updateMatrixWorld() } select() { var duplicates; function iterate(arr) { arr.forEach((it) => { if (it.type === 'group' && !duplicates) { if (it.name === selected_group.name && it !== selected_group) { duplicates = true } else if (it.children && it.children.length) { iterate(it.children) } } }) } iterate(TreeElements) if (duplicates) { Blockbench.showMessageBox({ translateKey: 'duplicate_groups', icon: 'folder', buttons: [tl('dialog.ok')], }) } Timeline.animator = this; Timeline.keyframes.forEach(function(kf) { kf.selected = false }) Timeline.selected.length = 0 Timeline.keyframes = Timeline.vue._data.keyframes = this.keyframes if (this.keyframes[0]) { this.keyframes[0].select() } else { updateKeyframeSelection() } if (this.group && this.group.parent && this.group.parent !== 'root') { this.group.parent.openUp() } Vue.nextTick(Timeline.update) return this; } } class Keyframe { constructor(data) { this.type = 'keyframe' this.channel = 'rotation'//, 'position', 'scale' this.channel_index = 0; this.time = 0; this.selected = 0; this.x = '0'; this.y = '0'; this.z = '0'; this.w = '0'; this.isQuaternion = false; this.uuid = guid() if (typeof data === 'object') { this.extend(data) if (this.channel === 'scale' && data.x === undefined) { this.x = 1 } } } get(axis) { if (!this[axis]) { return 0; } else if (!isNaN(this[axis])) { return parseFloat(this[axis]) } else { return this[axis] } } calc(axis) { return parseMolang(this[axis]) } set(axis, value) { if (axis === 'x' || axis === 'y' || axis === 'z' || axis === 'w') { this[axis] = value } return this; } offset(axis, amount) { var value = this.get(axis) if (!value || value === '0') { this.set(axis, 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.substr(0,1)=='-'?'':'+') + trimFloatNumber(number) } else { value = trimFloatNumber(amount) +(value.substr(0,1)=='-'?'':'+')+ value } } this.set(axis, value) return value; /* function iterate(string, index) { if (!isNaN(string)) { return [index, string.length] } var splices = splitUpMolang(string, ['+', '-']) if (!splices) return false; var result = iterate(splices[1], index+splices[0].length) if (result) return result; result = iterate(splices[0], index) if (result) return result; } var p = iterate(value, 0) if (p) { value = value.substr(0, p[0]) + amount + value.substr(p[0]+p[1]) this.set(axis, value) } else { amount = ''+amount if (amount.substr(0, 1) !== '-') amount = '+'+amount this.set(axis, value+amount) }*/ } 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() { if (this.channel === 'scale') { return this.get('x') } var arr = [ this.get('x'), this.get('y'), this.get('z'), ] if (this.channel === 'rotation' && this.isQuaternion) { arr.push(this.get('w')) } return arr; } select(event) { var scope = this; if (this.dragging) { delete this.dragging return this; } if (!event || (!event.shiftKey && !event.ctrlKey)) { Timeline.selected.forEach(function(kf) { kf.selected = false }) Timeline.selected.length = 0 } if (event && event.shiftKey && Timeline.selected.length) { var last = Timeline.selected[Timeline.selected.length-1] if (last && last.channel === scope.channel) { Timeline.keyframes.forEach((kf) => { if (kf.channel === scope.channel && Math.isBetween(kf.time, last.time, scope.time) && !kf.selected ) { kf.selected = true Timeline.selected.push(kf) } }) } } if (Timeline.selected.indexOf(this) == -1) { Timeline.selected.push(this) } this.selected = true updateKeyframeSelection() return this; } callMarker() { Timeline.setTime(this.time) Animator.preview() return this; } findNearest(distance, channel, direction) { if (!this.parent) return []; //channel: all, others, this, 0, 1, 2 //direction: true>, false<, undefined<> var scope = this function getDelta(kf, abs) { if (abs) { return Math.abs(kf.time - scope.time) } else { return kf.time - scope.time } } var matches = [] var i = 0; while (i < scope.parent.keyframes.length) { var kf = scope.parent.keyframes[i] let delta = getDelta(kf) let delta_match = Math.abs(delta) <= distance && (delta>0 == direction || direction === undefined) let channel_match = ( (channel === 'all') || (channel === 'others' && kf.channel !== scope.channel) || (channel === 'this' && kf.channel === scope.channel) || (channel === kf.channel_index) || (channel === kf.channel) ) if (channel_match && delta_match) { matches.push(kf) } i++; } matches.sort((a, b) => { return getDelta(a, true) - getDelta(b, true) }) return matches } showContextMenu(event) { if (!this.selected) { this.select(); } this.menu.open(event, this); return this; } remove() { if (this.parent) { this.parent.keyframes.remove(this) } Timeline.selected.remove(this) } extend(data) { if (data.channel && Animator.possible_channels[data.channel]) { Merge.string(this, data, 'channel') } else if (typeof data.channel === 'number') { this.channel = Animator.channel_index[data.channel] } Merge.number(this, data, 'time') 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') this.channel_index = Animator.channel_index.indexOf(this.channel) return this; } undoCopy() { var copy = { channel: this.channel_index, time: this.time, x: this.x, //uuid: this.uuid } if (this.channel_index !== 2) {//Not Scale copy.y = this.y copy.z = this.z } if (this.channel_index === 0 && this.isQuaternion) { copy.w = this.w } return copy; } } Keyframe.prototype.menu = new Menu([ {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() } }, {name: 'generic.delete', icon: 'delete', click: function(keyframe) { keyframe.select({shiftKey: true}) removeSelectedKeyframes() }}, /* settotimestamp delete */ ]) function updateKeyframeValue(obj) { var axis = $(obj).attr('axis') var value = $(obj).val() Timeline.selected.forEach(function(kf) { kf.set(axis, value) }) BARS.updateConditions() Animator.preview() } function updateKeyframeSelection() { if (!selected_group) { Timeline.keyframes = Timeline.vue._data.keyframes = [] Timeline.animator = undefined Timeline.selected.length = 0 } 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 } }) if (Timeline.selected.length && !multi_channel) { var first = Timeline.selected[0] $('#keyframe_type_label').text(tl('panel.keyframe.type', [tl('timeline.'+first.channel)] )) $('#keyframe_bar_x').show() $('#keyframe_bar_y, #keyframe_bar_z').toggle(first.channel !== 'scale') $('#keyframe_bar_w').toggle(first.channel === 'rotation' && first.isQuaternion) var values = [ first.get('x'), first.get('y'), first.get('z'), first.get('w') ] values.forEach((v, vi) => { if (typeof v === 'number') { values[vi] = trimFloatNumber(v) } }) $('#keyframe_bar_x input').val(values[0]) $('#keyframe_bar_y input').val(values[1]) $('#keyframe_bar_z input').val(values[2]) $('#keyframe_bar_w input').val(values[3]) 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() } 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, keep_saved: true}) var i = Timeline.keyframes.length; while (i > 0) { i--; let kf = Timeline.keyframes[i] if (Timeline.selected.includes(kf)) { kf.remove() } } updateKeyframeSelection() Undo.finishEdit('remove keyframes') } const Animator = { possible_channels: {rotation: true, position: true, scale: true}, channel_index: ['rotation', 'position', 'scale'], open: false, animations: [], frame: 0, interval: false, join: function() { Animator.open = true; selected.length = 0 updateSelection() if (quad_previews.enabled) { quad_previews.enabled_before = true } main_preview.fullscreen() main_preview.setNormalCamera() $('body').addClass('animation_mode') $('.m_edit').hide() if (!Animator.timeline_node) { Animator.timeline_node = $('#timeline').get(0) } $('#preview').append(Animator.timeline_node) updateInterface() if (!Timeline.is_setup) { Timeline.setup() } Timeline.update() if (outlines.children.length) { outlines.children.length = 0 Canvas.updateAllPositions() } if (Animator.selected) { Animator.selected.select() } if (isApp && !Prop.animation_path && !Animator.animations.length && Prop.file_path) { //Load findBedrockAnimation() } }, leave: function (argument) { Timeline.pause() Animator.open = false; Canvas.updateAllPositions() $('#timeline').detach() $('.m_edit').show() $('body').removeClass('animation_mode') resizeWindow() updateInterface() if (quad_previews.enabled_before) { openQuadView() } }, preview: function() { if (Animator.selected) { Animator.selected.displayFrame(Timeline.second) } }, loadFile: function(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, override: a.override_previous_animation, anim_time_update: a.anim_time_update, length: a.animation_length, blend_weight: a.blend_weight }).add() //Bones for (var bone_name in a.bones) { var b = a.bones[bone_name] var group = TreeElements.findRecursive('name', bone_name) if (group) { var ba = new BoneAnimator() animation.bones[group.uuid] = ba ba.uuid = group.uuid; //Channels for (var channel in b) { if (Animator.possible_channels[channel]) { if (typeof b[channel] === 'string' || typeof b[channel] === 'number' || (typeof b[channel] === 'object' && b[channel].constructor.name === 'Array')) { ba.addKeyframe(b[channel], 0, channel) } else if (typeof b[channel] === 'object') { for (var timestamp in b[channel]) { ba.addKeyframe(b[channel][timestamp], parseFloat(timestamp), channel) } } } } } } if (!Animator.selected) { animation.select() } } if (isApp && file.path) { Prop.animation_path = file.path } } }, buildFile: function(options) { if (typeof options !== 'object') { options = {} } var animations = {} Animator.animations.forEach(function(a) { var ani_tag = animations[a.name] = {} if (a.loop) ani_tag.loop = true if (a.length) ani_tag.animation_length = a.length if (a.override) ani_tag.override = true if (a.anim_time_update) ani_tag.anim_time_update = a.anim_time_update ani_tag.bones = {} for (var uuid in a.bones) { var group = a.bones[uuid].getGroup() if (group && a.bones[uuid].keyframes.length) { var bone_tag = ani_tag.bones[group.name] = {} var channels = {} //Saving Keyframes a.bones[uuid].keyframes.forEach(function(kf) { if (!channels[kf.channel]) { channels[kf.channel] = {} } let timecode = trimFloatNumber(Math.round(kf.time*60)/60) + '' 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]] } else { timecodes.sort().forEach((time) => { if (!bone_tag[channel]) { bone_tag[channel] = {} } bone_tag[channel][time] = channels[channel][time] }) } } } } } }) return { format_version: '1.8.0', animations: animations } } } const Timeline = { keyframes: [],//frames selected: [],//frames second: 0, playing: false, setTime: function(seconds, editing) { seconds = limitNumber(seconds, 0, 1000) Timeline.vue._data.marker = seconds Timeline.second = seconds if (!editing) { Timeline.setTimecode(seconds) } Timeline.updateSize() //Scroll var scroll = $('#timeline_inner').scrollLeft() var marker = Timeline.second * Timeline.vue._data.size + 8 if (marker < scroll || marker > scroll + $('#timeline_inner').width()) { $('#timeline_inner').scrollLeft(marker-16) } }, setTimecode: function(time) { let m = Math.floor(time/60) let s = Math.floor(time%60) let f = Math.floor((time%1) * 30) if ((s+'').length === 1) {s = '0'+s} if ((f+'').length === 1) {f = '0'+f} $('#timeline_corner').text(m + ':' + s + ':' + f) }, setup: function() { /* $('#timeline_inner #timeline_marker').draggable({ axis: 'x', distance: 2, start: function(event, ui) { Timeline.pause() }, drag: function(event, ui) { var difference = (ui.position.left - ui.originalPosition.left) / Timeline.vue._data.size; Timeline.second = limitNumber(Timeline.vue._data.marker + difference, 0, 1000) Timeline.setTimecode(Timeline.second) Timeline.updateSize() if (Animator.selected) { Animator.preview() } }, stop: function(event, ui) { Timeline.setTime(Timeline.second) } })*/ $('#timeline_inner #timeline_time').mousedown(e => { Timeline.dragging_marker = true; let time = e.offsetX / Timeline.vue._data.size Timeline.setTime(time) if (Animator.selected) { Animator.preview() } }) $(document).mousemove(e => { if (Timeline.dragging_marker) { let offset = mouse_pos.x - $('#timeline_inner #timeline_time').offset().left let time = offset / Timeline.vue._data.size Timeline.setTime(time) Animator.preview() } }) .mouseup(e => { if (Timeline.dragging_marker) { delete Timeline.dragging_marker } }) //Keyframe inputs $('.keyframe_input').click(e => { Undo.initEdit({keyframes: Timeline.selected, keep_saved: true}) }).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.second) } }) .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]/30, 0, 29) if (Math.abs(seconds-Timeline.second) > 1e-3 ) { Timeline.setTime(seconds, true) if (Animator.selected) { Animator.preview() } } }) Timeline.is_setup = true Timeline.setTime(0) }, update: function() { //Draggable $('#timeline_inner .keyframe').draggable({ axis: 'x', distance: 10, start: function(event, ui) { Undo.initEdit({keyframes: Timeline.keyframes, keep_saved: true}) var id = $(ui.helper).attr('id') var i = 0; while (i < Timeline.vue._data.keyframes.length) { var kf = Timeline.vue._data.keyframes[i] if (kf.uuid === id || kf.selected) { kf.time_before = kf.time } i++; } }, drag: function(event, ui) { var difference = (ui.position.left - ui.originalPosition.left - 8) / Timeline.vue._data.size; var id = $(ui.helper).attr('id') var snap_value = false var nearest var i = 0; while (i < Timeline.vue._data.keyframes.length) { var kf = Timeline.vue._data.keyframes[i] if (kf.uuid === id) { i = Infinity kf.time = limitNumber(kf.time_before + difference, 0, 256) nearest = kf.findNearest(8 / Timeline.vue._data.size, 'others') } i++; } if (nearest && nearest.length) { snap_value = nearest[0].time difference = snap_value - kf.time_before; } var i = 0; while (i < Timeline.vue._data.keyframes.length) { var kf = Timeline.vue._data.keyframes[i] if (kf.uuid === id || kf.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 = t } i++; } BarItems.slider_keyframe_time.update() Animator.preview() }, stop: function(event, ui) { var id = $(ui.helper).attr('id') var i = 0; while (i < Timeline.vue._data.keyframes.length) { var kf = Timeline.vue._data.keyframes[i] if (kf.uuid === id) { kf.dragging = true } i++; } Undo.finishEdit('drag keyframes') } }) }, updateSize: function() { let size = Timeline.vue._data.size var max_length = ($('#timeline_inner').width()-8) / Timeline.vue._data.size; Timeline.vue._data.keyframes.forEach((kf) => { max_length = Math.max(max_length, kf.time) }) max_length = Math.max(max_length, Timeline.second) Timeline.vue._data.length = max_length Timeline.vue._data.timecodes.length = 0 var step = 1 if (size < 1) {step = 1} else if (size < 20) {step = 4} else if (size < 40) {step = 2} else if (size < 90) {step = 1} else if (size < 180) {step = 0.5} else if (size < 400) {step = 0.2} else if (size < 800) {step = 0.1} else {step = 0.05} var i = 0; while (i < Timeline.vue._data.length) { Timeline.vue._data.timecodes.push({ time: i, text: Math.round(i*100)/100 }) i += step; } }, unselect: function(e) { if (!Animator.selected) return; Timeline.keyframes.forEach((kf) => { if (kf.selected) { Timeline.selected.remove(kf) } kf.selected = false }) updateKeyframeSelection() }, start: function() { if (!Animator.selected) return; Animator.selected.getMaxLength() Timeline.pause() Timeline.playing = true BarItems.play_animation.setIcon('pause') Timeline.loop() }, loop: function() { Animator.preview() if (Animator.selected && Timeline.second < (Animator.selected.length||1e3)) { Animator.interval = setTimeout(Timeline.loop, 16.66) Timeline.setTime(Timeline.second + 1/60) } else { Timeline.setTime(0) if (Animator.selected && Animator.selected.loop) { Timeline.start() } else { Timeline.pause() } } }, pause: function() { Timeline.playing = false; BarItems.play_animation.setIcon('play_arrow') if (Animator.interval) { clearInterval(Animator.interval) Animator.interval = false } }, addKeyframe: function(channel) { if (!Animator.selected) { Blockbench.showQuickMessage('message.no_animation_selected') return } var bone = Animator.selected.getBoneAnimator() if (!bone) { Blockbench.showQuickMessage('message.no_bone_selected') return } Undo.initEdit({keyframes: bone.keyframes, keep_saved: true}) var kf = bone.addKeyframe(false, Timeline.second, channel?channel:'rotation') kf.select() Undo.finishEdit('add_keyframe') Vue.nextTick(Timeline.update) }, showMenu: function(event) { if (event.target.id === 'timeline_inner') { Timeline.menu.open(event, event); } }, menu: new Menu([ {name: 'menu.timeline.add', icon: 'add', click: function(context) { var time = (context.offsetX+$('#timeline_inner').scrollLeft()-8) / Timeline.vue._data.size var row = Math.floor((context.offsetY-32) / 31 + 0.15) if (!Animator.selected) { Blockbench.showQuickMessage('message.no_animation_selected') return; } var bone = Animator.selected.getBoneAnimator() if (bone) { Undo.initEdit({keyframes: bone.keyframes, keep_saved: true}) var kf = bone.addKeyframe(false, Math.round(time*30)/30, row === 2 ? 'scale' : (row === 1 ? 'position' : 'rotation')) kf.select().callMarker() Vue.nextTick(Timeline.update) Undo.finishEdit('add_keyframe') } else { Blockbench.showQuickMessage('message.no_bone_selected') } }} ]) } onVueSetup(function() { Animator.vue = new Vue({ el: '#animations_list', data: { animations: Animator.animations } }) Timeline.vue = new Vue({ el: '#timeline_inner', data: { size: 150, length: 10, timecodes: [], keyframes: [], marker: Timeline.second } }) }) BARS.defineActions(function() { new Action({ id: 'add_animation', icon: 'fa-plus-circle', category: 'animation', condition: () => Animator.open, click: function () { var animation = new Animation({ name: 'animation.' + (Project.parent||'model') + '.new' }).add().select() } }) new Action({ id: 'load_animation_file', icon: 'fa-file-video-o', category: 'animation', condition: () => Animator.open, click: function () { var path = Prop.file_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(Prop.file_path).replace(/\.geo/, '.animation') } } Blockbench.import({ extensions: ['json'], type: 'JSON Animation', startpath: path }, function(files) { Animator.loadFile(files[0]) }) } }) new Action({ id: 'export_animation_file', icon: 'save', category: 'animation', condition: () => Animator.open, click: function () { var content = autoStringify(Animator.buildFile()) var path = Prop.animation_path if (isApp && !path) { path = Prop.file_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(Prop.file_path, true) } } Blockbench.export({ type: 'JSON Animation', extensions: ['json'], name: Project.parent||'animation', startpath: path, content: content }) } }) new Action({ id: 'play_animation', icon: 'play_arrow', category: 'animation', keybind: new Keybind({key: 32}), condition: () => Animator.open, click: function () { if (!Animator.selected) { Blockbench.showQuickMessage('message.no_animation_selected') return; } if (Timeline.playing) { Timeline.pause() } else { Timeline.start() } } }) new NumSlider({ id: 'slider_animation_length', category: 'animation', condition: () => Animator.open && Animator.selected, get: function() { return Animator.selected.length }, change: function(value, fixed) { if (!fixed) { value += Animator.selected.length } Animator.selected.length = limitNumber(value, 0, 1e4) } }) new NumSlider({ id: 'slider_keyframe_time', category: 'animation', condition: () => Animator.open && Timeline.selected.length, get: function() { return Timeline.selected[0] ? Timeline.selected[0].time : 0 }, change: function(value, fixed) { Timeline.selected.forEach((kf) => { if (!fixed) { value += kf.time } kf.time = limitNumber(value, 0, 1e4) }) Animator.preview() }, onBefore: function() { Undo.initEdit({keyframes: Timeline.selected, keep_saved: true}) }, onAfter: function() { Undo.finishEdit('edit keyframe') } }) new Action({ id: 'select_all_keyframes', icon: 'select_all', category: 'animation', condition: () => Animator.open, keybind: new Keybind({key: 65, ctrl: true}), click: function () {selectAllKeyframes()} }) new Action({ id: 'delete_keyframes', icon: 'delete', category: 'animation', condition: () => Animator.open, keybind: new Keybind({key: 46}), click: function () {removeSelectedKeyframes()} }) })