Merge branch 'bezier-keyframes' into next

This commit is contained in:
JannisX11 2022-12-21 15:36:56 +01:00
commit 2c4067ecb7
7 changed files with 318 additions and 31 deletions

View File

@ -1087,6 +1087,34 @@
z-index: 4;
}
.keyframe_bezier_handle {
background-color: var(--color-back);
border: 2px solid var(--color-text);
border-radius: 50%;
height: 10px;
width: 10px;
margin: 1px;
margin-top: 3px;
cursor: move;
position: absolute;
}
.keyframe_bezier_handle:hover, .keyframe_bezier_handle:active {
border-color: var(--color-accent);
}
.keyframe_bezier_handle::after {
content: "";
display: block;
position: relative;
background-color: var(--color-grid);
height: 2px;
width: var(--length);
transform-origin: left;
rotate: var(--angle);
top: 2px;
left: 3px;
pointer-events: none;
}
#timeline_header {
height: 28px;
display: flex;

View File

@ -193,37 +193,66 @@ class Animation extends AnimationItem {
bone_tag.relative_to = {rotation: 'entity'};
bone_tag.rotation = [0, 0, 0.01];
}
//Saving Keyframes
animator.keyframes.forEach(function(kf) {
if (!channels[kf.channel]) {
channels[kf.channel] = {};
}
let timecode = kf.getTimecodeString();
channels[kf.channel][timecode] = kf.compileBedrockKeyframe()
if (animator.rotation_global && kf.channel == 'rotation' && channels[kf.channel][timecode] instanceof Array && channels[kf.channel][timecode].allEqual(0)) {
channels[kf.channel][timecode][2] = 0.01;
}
})
// Sorting + compressing keyframes
for (var channel in Animator.possible_channels) {
if (channels[channel]) {
let timecodes = Object.keys(channels[channel])
if (timecodes.length === 1 && animator[channel][0].data_points.length == 1 && animator[channel][0].interpolation != 'catmullrom') {
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];
if (!animator[channel]?.length) continue;
// Saving Keyframes
bone_tag[channel] = {};
let sorted_keyframes = animator[channel].slice().sort((a, b) => a.time - b.time);
sorted_keyframes.forEach((kf, i) => {
let timecode = kf.getTimecodeString();
bone_tag[channel][timecode] = kf.compileBedrockKeyframe()
if (animator.rotation_global && kf.channel == 'rotation' && bone_tag[kf.channel][timecode] instanceof Array && bone_tag[kf.channel][timecode].allEqual(0)) {
bone_tag[kf.channel][timecode][2] = 0.01;
}
// Bake bezier keyframe curve
let next_keyframe = sorted_keyframes[i+1];
if (next_keyframe && kf.interpolation === 'bezier' && next_keyframe.interpolation === 'bezier') {
let interval = 1 / this.snapping;
let interpolated_values = {};
for (let time = kf.time + interval; time < next_keyframe.time + (interval/2); time += interval) {
let itimecode = trimFloatNumber(Timeline.snapTime(time, this)).toString();
if (!itimecode.includes('.')) itimecode += '.0';
let lerp = Math.getLerp(kf.time, next_keyframe.time, time)
let value = [0, 1, 2].map(axis => {
return kf.getBezierLerp(kf, next_keyframe, getAxisLetter(axis), lerp);
})
interpolated_values[itimecode] = value;
}
} else {
timecodes.sort((a, b) => parseFloat(a) - parseFloat(b)).forEach((timecode) => {
if (!bone_tag[channel]) {
bone_tag[channel] = {}
// Optimize data
let itimecodes = Object.keys(interpolated_values);
let skipped = 0;
let threshold = channel == 'scale' ? 0.01 : 0.1;
itimecodes.forEach((itimecode, ti) => {
let value = interpolated_values[itimecode]
let last = interpolated_values[itimecodes[ti-1]] || bone_tag[channel][timecode];
let next = interpolated_values[itimecodes[ti+1]];
if (!next) return;
let all_axes_irrelevant = value.allAre((val, axis) => {
let diff = Math.abs((last[axis] - val) - (val - next[axis]));
return diff < threshold
})
if (all_axes_irrelevant && skipped < 3) {
skipped++;
} else {
bone_tag[channel][itimecode] = value;
skipped = 0;
}
bone_tag[channel][timecode] = channels[channel][timecode];
})
}
})
// Compressing keyframes
let timecodes = Object.keys(bone_tag[channel]);
if (timecodes.length === 1 && animator[channel][0].data_points.length == 1 && animator[channel][0].interpolation != 'catmullrom') {
bone_tag[channel] = bone_tag[channel][timecodes[0]]
if (channel == 'scale' &&
bone_tag[channel][timecodes[0]] instanceof Array &&
bone_tag[channel][timecodes[0]].allEqual(bone_tag[channel][timecodes[0]][0])
) {
bone_tag[channel] = bone_tag[channel][timecodes[0]][0];
}
}
}
}

View File

@ -196,6 +196,48 @@ class Keyframe {
return curve.getPoint(time).y;
}
getBezierLerp(before, after, axis, alpha) {
let axis_num = getAxisNumber(axis);
let val_before = before.calc(axis, 1);
let val_after = after.calc(axis, 0);
let time_gap = after.time - before.time;
let time_handle_before = Math.clamp(before.bezier_right_time[axis_num] || 0, 0, time_gap);
let time_handle_after = Math.clamp(after.bezier_left_time[axis_num] || 0, -time_gap, 0);
let vectors = [
new THREE.Vector2(before.time, val_before),
new THREE.Vector2(before.time + time_handle_before, val_before + before.bezier_right_value[axis_num] || 0),
new THREE.Vector2(after.time + time_handle_after, val_after + after.bezier_left_value[axis_num] || 0),
new THREE.Vector2(after.time, val_after),
];
let curve = new THREE.CubicBezierCurve(...vectors);
let time = before.time + (after.time - before.time) * alpha;
let points = curve.getPoints(200);
let closest;
let closest_diff = Infinity;
points.forEach(point => {
let diff = Math.abs(point.x - time);
if (diff < closest_diff) {
closest_diff = diff;
closest = point;
}
})
let second_closest;
closest_diff = Infinity;
points.forEach(point => {
if (point == closest) return;
let diff = Math.abs(point.x - time);
if (diff < closest_diff) {
closest_diff = diff;
second_closest = closest;
second_closest = point;
}
})
return Math.lerp(closest.y, second_closest.y, Math.clamp(Math.getLerp(closest.x, second_closest.x, time), 0, 1));
}
getArray(data_point = 0) {
var arr = [
this.get('x', data_point),
@ -247,7 +289,7 @@ class Keyframe {
compileBedrockKeyframe() {
if (this.transform) {
if (this.interpolation != 'linear' && this.interpolation != 'step') {
if (this.interpolation == 'catmullrom') {
let previous = this.getPreviousKeyframe();
let include_pre = (!previous && this.time > 0) || (previous && previous.interpolation != 'catmullrom')
return {
@ -468,6 +510,8 @@ class Keyframe {
'_',
'keyframe_uniform',
'keyframe_interpolation',
'keyframe_bezier_linked',
'reset_keyframe',
{name: 'menu.cube.color', icon: 'color_lens', children() {
return [
{icon: 'bubble_chart', name: 'generic.unset', click: function(kf) {kf.forSelected(kf2 => {kf2.color = -1}, 'change color')}},
@ -481,6 +525,7 @@ class Keyframe {
}})
];
}},
'_',
'copy',
'delete',
])
@ -488,10 +533,16 @@ class Keyframe {
new Property(Keyframe, 'number', 'color', {default: -1})
new Property(Keyframe, 'boolean', 'uniform', {condition: keyframe => keyframe.channel == 'scale', default: settings.uniform_keyframe.value})
new Property(Keyframe, 'string', 'interpolation', {default: 'linear'})
new Property(Keyframe, 'boolean', 'bezier_linked', {default: true})
new Property(Keyframe, 'vector', 'bezier_left_time', {default: [-0.1, -0.1, -0.1]});
new Property(Keyframe, 'vector', 'bezier_left_value');
new Property(Keyframe, 'vector', 'bezier_right_time', {default: [0.1, 0.1, 0.1]});
new Property(Keyframe, 'vector', 'bezier_right_value');
Keyframe.selected = [];
Keyframe.interpolation = {
linear: 'linear',
catmullrom: 'catmullrom',
bezier: 'bezier',
step: 'step',
}
@ -819,18 +870,41 @@ BARS.defineActions(function() {
updateKeyframeSelection();
}
})
new Toggle('keyframe_bezier_linked', {
icon: 'fa-bezier-curve',
category: 'animation',
condition: () => Animator.open && Timeline.selected.length && !Timeline.selected.find(kf => kf.interpolation !== 'bezier'),
onChange(value) {
let keyframes = Timeline.selected;
Undo.initEdit({keyframes})
keyframes.forEach((kf) => {
kf.bezier_linked = value;
if (value) {
kf.bezier_right_time.V3_set(kf.bezier_left_time).V3_multiply(-1);
kf.bezier_right_value.V3_set(kf.bezier_left_value).V3_multiply(-1);
}
})
Timeline.vue.show_zero_line = !Timeline.vue.show_zero_line;
Timeline.vue.show_zero_line = !Timeline.vue.show_zero_line;
Undo.finishEdit('Change keyframes bezier link option');
updateKeyframeSelection();
}
})
new BarSelect('keyframe_interpolation', {
category: 'animation',
condition: () => Animator.open && Timeline.selected.length && Timeline.selected.find(kf => kf.transform),
options: {
linear: true,
catmullrom: true,
bezier: true,
step: true,
},
onChange: function(sel, event) {
Undo.initEdit({keyframes: Timeline.selected})
Timeline.selected.forEach((kf) => {
if (kf.transform) kf.interpolation = sel.value;
if (kf.transform) {
kf.interpolation = sel.value;
}
})
Undo.finishEdit('Change keyframes interpolation')
updateKeyframeSelection();
@ -844,6 +918,12 @@ BARS.defineActions(function() {
Undo.initEdit({keyframes: Timeline.selected})
Timeline.selected.forEach((kf) => {
kf.data_points.replace([new KeyframeDataPoint(kf)]);
if (kf.interpolation == 'bezier') {
kf.bezier_left_time.V3_set(-0.1, -0.1, -0.1);
kf.bezier_left_value.V3_set(0, 0, 0);
kf.bezier_right_time.V3_set(0.1, 0.1, 0.1);
kf.bezier_right_value.V3_set(0, 0, 0);
}
})
Undo.finishEdit('Reset keyframes')
updateKeyframeSelection()

View File

@ -698,6 +698,14 @@ Interface.definePanels(() => {
max = Math.max(max, value);
}
}
keyframes.forEach(kf => {
if (kf.interpolation === 'bezier') {
min = Math.min(min, kf.display_value + kf.bezier_left_value[this.graph_editor_axis_number]);
max = Math.max(max, kf.display_value + kf.bezier_left_value[this.graph_editor_axis_number]);
min = Math.min(min, kf.display_value + kf.bezier_right_value[this.graph_editor_axis_number]);
max = Math.max(max, kf.display_value + kf.bezier_right_value[this.graph_editor_axis_number]);
}
})
Timeline.time = original_time;
@ -722,6 +730,9 @@ Interface.definePanels(() => {
}
return string;
},
graph_editor_axis_number() {
return getAxisNumber(this.graph_editor_axis)
}
},
methods: {
@ -824,6 +835,7 @@ Interface.definePanels(() => {
},
dragKeyframes(clicked, e1) {
convertTouchEvent(e1);
if (e1.target.classList.contains('keyframe_bezier_handle')) return;
let dragging_range;
let dragging_restriction;
let originalValue;
@ -996,6 +1008,122 @@ Interface.definePanels(() => {
addEventListeners(document, 'mousemove touchmove', slide, {passive: false});
addEventListeners(document, 'mouseup touchend', off);
},
dragBezierHandle(clicked, side, e1) {
convertTouchEvent(e1);
let values_changed;
let is_setup = false;
let axis_number = getAxisNumber(this.graph_editor_axis);
let old_values = {};
function setup() {
if (!clicked.selected && !e1.shiftKey && !Pressing.overrides.shift && Timeline.selected.length != 0) {
clicked.select()
} else if (clicked && !clicked.selected) {
clicked.select({shiftKey: true})
}
Keyframe.selected.forEach(kf => {
if (kf.interpolation == 'bezier') {
old_values[kf.uuid] = {
bezier_left_time: kf.bezier_left_time.slice(),
bezier_left_value: kf.bezier_left_value.slice(),
bezier_right_time: kf.bezier_right_time.slice(),
bezier_right_value: kf.bezier_right_value.slice(),
}
}
})
Undo.initEdit({keyframes: Timeline.selected});
Timeline.dragging_keyframes = true;
is_setup = true;
}
function slide(e2) {
convertTouchEvent(e2);
e2.preventDefault();
let offset = [
e2.clientX - e1.clientX,
e2.clientY - e1.clientY,
]
if (!is_setup) {
if (Math.pow(offset[0], 2) + Math.pow(offset[1], 2) > 20) {
setup();
} else {
return;
}
}
var difference_time = Math.clamp(offset[0] / Timeline.vue._data.size, -256, 256);
var difference_value = Math.clamp(-offset[1] / Timeline.vue.graph_size, -256, 256);
for (var kf of Timeline.selected) {
if (kf.interpolation == 'bezier') {
kf.bezier_left_time.V3_set(old_values[kf.uuid].bezier_left_time);
kf.bezier_left_value.V3_set(old_values[kf.uuid].bezier_left_value);
kf.bezier_right_time.V3_set(old_values[kf.uuid].bezier_right_time);
kf.bezier_right_value.V3_set(old_values[kf.uuid].bezier_right_value);
if (side === 'left') {
kf.bezier_left_time[axis_number] = Math.min(0, old_values[kf.uuid].bezier_left_time[axis_number] + difference_time);
kf.bezier_left_value[axis_number] = old_values[kf.uuid].bezier_left_value[axis_number] + difference_value;
if (kf.bezier_linked) {
kf.bezier_right_time[axis_number] = -kf.bezier_left_time[axis_number];
kf.bezier_right_value[axis_number] = -kf.bezier_left_value[axis_number];
}
}
if (side === 'right') {
kf.bezier_right_time[axis_number] = Math.max(0, old_values[kf.uuid].bezier_right_time[axis_number] + difference_time);
kf.bezier_right_value[axis_number] = old_values[kf.uuid].bezier_right_value[axis_number] + difference_value;
if (kf.bezier_linked) {
kf.bezier_left_time[axis_number] = -kf.bezier_right_time[axis_number];
kf.bezier_left_value[axis_number] = -kf.bezier_right_value[axis_number];
}
}
values_changed = true;
}
}
let text = `${trimFloatNumber(Math.roundTo(difference_time, 2))}${trimFloatNumber(Math.roundTo(difference_value, 2))}`;
Blockbench.setStatusBarText(text);
Timeline.vue.show_zero_line = !Timeline.vue.show_zero_line;
Timeline.vue.show_zero_line = !Timeline.vue.show_zero_line;
Animator.showMotionTrail()
Animator.preview()
}
function off() {
removeEventListeners(document, 'mousemove touchmove', slide);
removeEventListeners(document, 'mouseup touchend', off);
if (is_setup) {
Blockbench.setStatusBarText();
if (values_changed) {
Undo.finishEdit('Adjust keyframe bezier handles');
} else {
Undo.cancelEdit();
}
setTimeout(() => {
Timeline.dragging_keyframes = false;
}, 20);
}
}
addEventListeners(document, 'mousemove touchmove', slide, {passive: false});
addEventListeners(document, 'mouseup touchend', off);
},
getBezierHandleStyle(keyframe, side) {
let axis_number = getAxisNumber(this.graph_editor_axis);
let x_offset = -keyframe[`bezier_${side}_time`][axis_number] * this.size;
let y_offset = -keyframe[`bezier_${side}_value`][axis_number] * this.graph_size;
let length = Math.sqrt(Math.pow(x_offset, 2) + Math.pow(y_offset, 2));
let angle = Math.atan2(-y_offset, x_offset);
return {
right: x_offset + 'px',
top: y_offset + 'px',
'--length': Math.max(length - 6, 0) + 'px',
'--angle': Math.radToDeg(angle) + 'deg',
}
},
clamp: Math.clamp,
trimFloatNumber
},
@ -1149,6 +1277,19 @@ Interface.definePanels(() => {
<i class="material-icons keyframe_icon_smaller" v-if="keyframe.interpolation == 'catmullrom'">lens</i>
<i class="material-icons keyframe_icon_step" v-else-if="keyframe.interpolation == 'step'">eject</i>
<i :class="keyframe.data_points.length == 1 ? 'icon-keyframe' : 'icon-keyframe_discontinuous'" v-else></i>
<template v-if="keyframe.interpolation == 'bezier'">
<div class="keyframe_bezier_handle"
:style="getBezierHandleStyle(keyframe, 'left')"
:title="'${tl('generic.left')}: ' + trimFloatNumber(keyframe.bezier_left_time[graph_editor_axis_number]) + ' ⨉ ' + trimFloatNumber(keyframe.bezier_left_value[graph_editor_axis_number])"
@mousedown="dragBezierHandle(keyframe, 'left', $event)" @touchstart="dragBezierHandle('left', $event)"
></div>
<div class="keyframe_bezier_handle"
:style="getBezierHandleStyle(keyframe, 'right')"
:title="'${tl('generic.right')}: ' + trimFloatNumber(keyframe.bezier_right_time[graph_editor_axis_number]) + ' ⨉ ' + trimFloatNumber(keyframe.bezier_right_value[graph_editor_axis_number])"
@mousedown="dragBezierHandle(keyframe, 'right', $event)" @touchstart="dragBezierHandle('right', $event)"
></div>
</template>
</div>
</template>
</div>

View File

@ -393,13 +393,15 @@ class BoneAnimator extends GeneralAnimator {
} else {
let no_interpolations = Blockbench.hasFlag('no_interpolations')
let alpha = Math.getLerp(before.time, after.time, time)
if (no_interpolations || (before.interpolation !== Keyframe.interpolation.catmullrom && after.interpolation !== Keyframe.interpolation.catmullrom)) {
if (no_interpolations || (before.interpolation === Keyframe.interpolation.linear && after.interpolation === Keyframe.interpolation.linear)) {
if (no_interpolations) {
alpha = Math.round(alpha)
}
return mapAxes(axis => before.getLerp(after, axis, alpha, allow_expression));
} else {
} else if (before.interpolation === Keyframe.interpolation.catmullrom || after.interpolation === Keyframe.interpolation.catmullrom) {
let sorted = this[channel].slice().sort((kf1, kf2) => (kf1.time - kf2.time));
let before_index = sorted.indexOf(before);
@ -407,6 +409,10 @@ class BoneAnimator extends GeneralAnimator {
let after_plus = sorted[before_index+2];
return mapAxes(axis => before.getCatmullromLerp(before_plus, before, after, after_plus, axis, alpha));
} else if (before.interpolation === Keyframe.interpolation.bezier || after.interpolation === Keyframe.interpolation.bezier) {
// Bezier
return mapAxes(axis => before.getBezierLerp(before, after, axis, alpha));
}
}
if (result && result instanceof Keyframe) {

View File

@ -42,7 +42,7 @@ function buildAnimationTracks(do_quaternions = true) {
// Sampling non-linear and math-based values
let contains_script
for (var kf of keyframes) {
if (kf.interpolation == Keyframe.interpolation.catmullrom) {
if (kf.interpolation == Keyframe.interpolation.catmullrom || kf.interpolation == Keyframe.interpolation.bezier) {
contains_script = true; break;
}
for (var data_point of kf.data_points) {

View File

@ -1475,9 +1475,12 @@
"action.keyframe_interpolation.desc": "Select the keyframe interpolation mode",
"action.keyframe_interpolation.linear": "Linear",
"action.keyframe_interpolation.catmullrom": "Smooth",
"action.keyframe_interpolation.bezier": "Bézier",
"action.keyframe_interpolation.step": "Step",
"action.keyframe_uniform": "Uniform Scaling",
"action.keyframe_uniform.desc": "Enable uniform scaling on the selected keyframes",
"action.keyframe_bezier_linked": "Link Bézier Handles",
"action.keyframe_bezier_linked.desc": "Connect the right and left keyframe bezier handle",
"action.change_keyframe_file": "Select Keyframe File",
"action.change_keyframe_file.desc": "Select an audio or particle file to preview an effect",
"action.reset_keyframe": "Reset Keyframe",