mirror of
https://github.com/JannisX11/blockbench.git
synced 2025-02-17 16:20:13 +08:00
Merge branch 'animation-import' into 3.7
This commit is contained in:
commit
e48d2ccbd7
@ -394,13 +394,16 @@
|
||||
.panel#animations #animations_list {
|
||||
min-height: 126px;
|
||||
max-height: 294px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.animation {
|
||||
height: 42px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
padding: 8px;
|
||||
padding-left: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.animation:hover {
|
||||
@ -410,14 +413,15 @@
|
||||
background: var(--color-selected);
|
||||
}
|
||||
.animation > i {
|
||||
margin-top: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.animation > * {
|
||||
float: left;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.animation > .animation_name {
|
||||
width: calc(100% - 50px);
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
user-select: none;
|
||||
}
|
||||
.animation .animation_play_toggle {
|
||||
@ -427,6 +431,32 @@
|
||||
.animation .animation_play_toggle:hover {
|
||||
color: var(--color-light);
|
||||
}
|
||||
.animation .animation_save_button {
|
||||
width: 22px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.animation .animation_save_button:hover {
|
||||
color: var(--color-light);
|
||||
}
|
||||
.animation .animation_save_button:not(.clickable) {
|
||||
opacity: 0.5;
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
.animation_file_head {
|
||||
height: 28px;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
}
|
||||
.animation_file_head:hover {
|
||||
color: var(--color-light);
|
||||
}
|
||||
.animation_file_head > .icon-open-state {
|
||||
text-align: center;
|
||||
width: 21px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.panel#keyframe .tabs_small label {
|
||||
font-size: 1em;
|
||||
height: 30px;
|
||||
|
@ -95,7 +95,9 @@
|
||||
<script src="js/texturing/texture_generator.js"></script>
|
||||
<script src="js/texturing/color.js"></script>
|
||||
<script src="js/display_mode.js"></script>
|
||||
<script src="js/animations.js"></script>
|
||||
<script src="js/animations/animation.js"></script>
|
||||
<script src="js/animations/keyframe.js"></script>
|
||||
<script src="js/animations/timeline.js"></script>
|
||||
<script src="js/plugin_loader.js"></script>
|
||||
|
||||
<script src="js/io/project.js"></script>
|
||||
|
2558
js/animations.js
2558
js/animations.js
File diff suppressed because it is too large
Load Diff
1249
js/animations/animation.js
Normal file
1249
js/animations/animation.js
Normal file
File diff suppressed because it is too large
Load Diff
653
js/animations/keyframe.js
Normal file
653
js/animations/keyframe.js
Normal file
@ -0,0 +1,653 @@
|
||||
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])) {
|
||||
let num = parseFloat(this[axis]);
|
||||
return isNaN(num) ? 0 : num;
|
||||
} 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')
|
||||
)
|
||||
}
|
||||
}
|
||||
getTimecodeString() {
|
||||
let timecode = trimFloatNumber(Timeline.snapTime(this.time)).toString();
|
||||
if (!timecode.includes('.')) {
|
||||
timecode += '.0';
|
||||
}
|
||||
return timecode;
|
||||
}
|
||||
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')
|
||||
}
|
||||
|
||||
BARS.defineActions(function() {
|
||||
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('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('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 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_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()
|
||||
}
|
||||
})
|
||||
})
|
744
js/animations/timeline.js
Normal file
744
js/animations/timeline.js
Normal file
@ -0,0 +1,744 @@
|
||||
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);
|
||||
}}
|
||||
])
|
||||
|
||||
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)
|
||||
if (Animator.selected.loop == 'hold' && Timeline.time >= (Animator.selected.length||1e3)) {
|
||||
Timeline.setTime(0)
|
||||
}
|
||||
Timeline.loop()
|
||||
},
|
||||
loop() {
|
||||
Animator.preview()
|
||||
if (Animator.selected && Timeline.time < (Animator.selected.length||1e3)) {
|
||||
|
||||
let new_time = (Animator.selected && Animator.selected.anim_time_update)
|
||||
? Molang.parse(Animator.selected.anim_time_update)
|
||||
: Timeline.time + (1/60);
|
||||
Timeline.setTime(Timeline.time + (new_time - Timeline.time) * (Timeline.playback_speed/100));
|
||||
|
||||
} else {
|
||||
if (Animator.selected.loop == 'once') {
|
||||
Timeline.setTime(0)
|
||||
Animator.preview()
|
||||
Timeline.pause()
|
||||
} 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',
|
||||
])
|
||||
}
|
||||
|
||||
onVueSetup(function() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
BARS.defineActions(function() {
|
||||
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_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 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('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;
|
||||
}
|
||||
})
|
||||
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);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
@ -304,34 +304,75 @@ function setupPanels() {
|
||||
component: {
|
||||
name: 'panel-animations',
|
||||
data() { return {
|
||||
animations: Animator.animations
|
||||
animations: Animator.animations,
|
||||
files_folded: {}
|
||||
}},
|
||||
methods: {
|
||||
sort(event) {
|
||||
let item = Animator.animations.splice(event.oldIndex, 1)[0];
|
||||
Animator.animations.splice(event.newIndex, 0, item);
|
||||
toggle(key) {
|
||||
this.files_folded[key] = !this.files_folded[key];
|
||||
this.$forceUpdate();
|
||||
},
|
||||
saveFile(key, file) {
|
||||
if (key && isApp) {
|
||||
file.animations.forEach(animation => {
|
||||
animation.save();
|
||||
})
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
files() {
|
||||
let files = {};
|
||||
this.animations.forEach(animation => {
|
||||
let key = animation.path || '';
|
||||
if (!files[key]) files[key] = {
|
||||
animations: [],
|
||||
name: animation.path ? pathToName(animation.path, true) : 'Unsaved',
|
||||
saved: true
|
||||
};
|
||||
if (!animation.saved) files[key].saved = false;
|
||||
files[key].animations.push(animation);
|
||||
})
|
||||
return files;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="toolbar_wrapper animations"></div>
|
||||
<ul id="animations_list" class="list" v-sortable="{onUpdate: sort, fallbackTolerance: 10, animation: 0, handle: ':not(.animation_play_toggle)'}">
|
||||
<li
|
||||
v-for="animation in animations"
|
||||
v-bind:class="{ selected: animation.selected }"
|
||||
v-bind:anim_id="animation.uuid"
|
||||
class="animation"
|
||||
v-on:click.stop="animation.select()"
|
||||
v-on:dblclick.stop="animation.rename()"
|
||||
:key="animation.uuid"
|
||||
@contextmenu.prevent.stop="animation.showContextMenu($event)"
|
||||
>
|
||||
<i class="material-icons">movie</i>
|
||||
<input class="animation_name" v-model="animation.name" disabled="true">
|
||||
<div class="animation_play_toggle" v-on:click.stop="animation.togglePlayingState()">
|
||||
<i v-if="animation.playing" class="fa_big far fa-play-circle"></i>
|
||||
<i v-else class="fa_big far fa-circle"></i>
|
||||
<ul id="animations_list" class="list">
|
||||
<li v-for="(file, key) in files" :key="key" class="animation_file">
|
||||
<div class="animation_file_head" v-on:click.stop="toggle(key)">
|
||||
<i v-on:click.stop="toggle(key)" class="icon-open-state fa" :class=\'{"fa-angle-right": files_folded[key], "fa-angle-down": !files_folded[key]}\'></i>
|
||||
{{ file.name }}
|
||||
<div class="animation_file_save_button" v-if="!file.saved" v-on:click.stop="saveFile(key, file)">
|
||||
<i class="material-icons">save</i>
|
||||
</div>
|
||||
</div>
|
||||
<ul v-if="!files_folded[key]">
|
||||
<li
|
||||
v-for="animation in file.animations"
|
||||
v-bind:class="{ selected: animation.selected }"
|
||||
v-bind:anim_id="animation.uuid"
|
||||
class="animation"
|
||||
v-on:click.stop="animation.select()"
|
||||
v-on:dblclick.stop="animation.rename()"
|
||||
:key="animation.uuid"
|
||||
@contextmenu.prevent.stop="animation.showContextMenu($event)"
|
||||
>
|
||||
<i class="material-icons">movie</i>
|
||||
<input class="animation_name" v-model="animation.name" disabled="true">
|
||||
<div class="animation_save_button" v-bind:class="{clickable: !animation.saved}" v-on:click.stop="animation.save()">
|
||||
<i v-if="animation.saved" class="material-icons">check_circle</i>
|
||||
<i v-else class="material-icons">save</i>
|
||||
</div>
|
||||
<div class="animation_play_toggle" v-on:click.stop="animation.togglePlayingState()">
|
||||
<i v-if="animation.playing" class="fa_big far fa-play-circle"></i>
|
||||
<i v-else class="fa_big far fa-circle"></i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -65,6 +65,7 @@ window.BedrockEntityManager = {
|
||||
BedrockEntityManager.client_entity = BedrockEntityManager.getEntityFile();
|
||||
if (BedrockEntityManager.client_entity && BedrockEntityManager.client_entity.description) {
|
||||
|
||||
// Textures
|
||||
var tex_list = BedrockEntityManager.client_entity.description.textures
|
||||
if (tex_list instanceof Object) {
|
||||
var valid_textures_list = [];
|
||||
@ -129,6 +130,41 @@ window.BedrockEntityManager = {
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
var anim_list = BedrockEntityManager.client_entity.description.animations
|
||||
if (anim_list instanceof Object) {
|
||||
let animation_names = [];
|
||||
for (var key in anim_list) {
|
||||
if (anim_list[key].match && anim_list[key].match(/^animation\./)) {
|
||||
animation_names.push(anim_list[key]);
|
||||
}
|
||||
}
|
||||
// get all paths in folder
|
||||
let anim_files = [];
|
||||
function searchFolder(path) {
|
||||
try {
|
||||
var files = fs.readdirSync(path);
|
||||
for (var name of files) {
|
||||
var new_path = path + osfs + name;
|
||||
if (name.match(/\.json$/)) {
|
||||
anim_files.push(new_path);
|
||||
} else if (!name.includes('.')) {
|
||||
searchFolder(new_path);
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
searchFolder(PathModule.join(BedrockEntityManager.root_path, 'animations'));
|
||||
|
||||
anim_files.forEach(path => {
|
||||
try {
|
||||
let content = fs.readFileSync(path, 'utf8');
|
||||
Animator.loadFile({path, content}, animation_names);
|
||||
} catch (err) {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
BedrockEntityManager.findEntityTexture(Project.geometry_name)
|
||||
}
|
||||
|
@ -1099,6 +1099,7 @@
|
||||
"menu.animation.loop.loop": "Loop",
|
||||
"menu.animation.override": "Override",
|
||||
"menu.animation.anim_time_update": "Update Variable",
|
||||
"menu.animation.save": "Save",
|
||||
|
||||
"menu.keyframe.quaternion": "Quaternion",
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user