Improve timeline graph editor, render keyframes

This commit is contained in:
JannisX11 2021-01-20 20:31:54 +01:00
parent 566fdfdc2c
commit 6cf0fe4230
2 changed files with 75 additions and 31 deletions

View File

@ -714,7 +714,7 @@
margin-left: -2px; margin-left: -2px;
} }
#timeline_body .keyframe { #timeline .keyframe {
position: absolute; position: absolute;
margin-left: -6px; margin-left: -6px;
z-index: 3; z-index: 3;
@ -722,14 +722,14 @@
width: 13.5px; width: 13.5px;
height: 23px; height: 23px;
} }
#timeline_body .keyframe i { #timeline .keyframe i {
margin-top: 2px; margin-top: 2px;
transform: rotate(45deg); transform: rotate(45deg);
font-size: 14pt; font-size: 14pt;
margin-left: -3px; margin-left: -3px;
pointer-events: none; pointer-events: none;
} }
#timeline_body .keyframe i.keyframe_icon_smaller { #timeline .keyframe i.keyframe_icon_smaller {
font-size: 11pt; font-size: 11pt;
margin-top: 4px; margin-top: 4px;
margin-left: -1px; margin-left: -1px;
@ -743,7 +743,7 @@
font-size: 6pt; font-size: 6pt;
color: var(--color-grid); color: var(--color-grid);
} }
#timeline_body .keyframe.selected { #timeline .keyframe.selected {
color: var(--color-accent) !important; color: var(--color-accent) !important;
z-index: 4; z-index: 4;
} }
@ -916,20 +916,31 @@
#timeline_graph_editor { #timeline_graph_editor {
position: absolute; position: absolute;
pointer-events: none; top: 0px;
top: 28px; bottom: 0px;
bottom: 9px; left: 0px;
right: 6px; right: 0px;
} }
#timeline_graph_editor svg { #timeline_graph_editor svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin-left: 9px;
pointer-events: none;
} }
#timeline_graph_editor svg path { #timeline_graph_editor svg path {
stroke: #f72858; stroke: #f72858;
stroke-width: 2px; stroke-width: 2px;
fill: none; fill: none;
} }
#timeline_graph_editor .keyframe {
height: 16px;
width: 11px;
font-size: 10px;
}
#timeline_graph_editor .keyframe > i {
margin-top: 0px;
font-size: 16px;
}
/*UV*/ /*UV*/

View File

@ -57,6 +57,7 @@ const Timeline = {
if (e.which !== 1 || ( if (e.which !== 1 || (
!e.target.classList.contains('keyframe_section') && !e.target.classList.contains('keyframe_section') &&
!e.target.classList.contains('animator_head_bar') && !e.target.classList.contains('animator_head_bar') &&
e.target.id !== 'timeline_graph_editor' &&
e.target.id !== 'timeline_body_inner' e.target.id !== 'timeline_body_inner'
)) { )) {
return return
@ -123,16 +124,20 @@ const Timeline = {
channels[kf.channel] != false && channels[kf.channel] != false &&
(!channels.hide_empty || animator[kf.channel].length) (!channels.hide_empty || animator[kf.channel].length)
) { ) {
var channel_index = 0 //animator.channels.indexOf(kf.channel); if (!Timeline.vue.graph_editor_open) {
for (var channel of animator.channels) {
if (kf.channel == channel) break;
if (channels[channel] != false && (!channels.hide_empty || animator[channel].length)) {
channel_index++;
}
}
height = offset + channel_index*24 + 36; var channel_index = 0 //animator.channels.indexOf(kf.channel);
for (var channel of animator.channels) {
if (kf.channel == channel) break;
if (channels[channel] != false && (!channels.hide_empty || animator[channel].length)) {
channel_index++;
}
}
var height = offset + channel_index*24 + 36;
} else {
var height = Timeline.vue.graph_offset - (kf.display_value || 0) * Timeline.vue.graph_size + Timeline.vue.scroll_top;
}
if (height > rect.ay && height < rect.by) { if (height > rect.ay && height < rect.by) {
kf.selected = true; kf.selected = true;
Timeline.selected.push(kf); Timeline.selected.push(kf);
@ -369,6 +374,7 @@ const Timeline = {
}); });
$('#timeline_body').on('scroll', e => { $('#timeline_body').on('scroll', e => {
Timeline.vue._data.scroll_left = $('#timeline_body').scrollLeft()||0; Timeline.vue._data.scroll_left = $('#timeline_body').scrollLeft()||0;
Timeline.vue._data.scroll_top = $('#timeline_body').scrollTop()||0;
}) })
BarItems.slider_animation_speed.update() BarItems.slider_animation_speed.update()
@ -644,6 +650,7 @@ onVueSetup(function() {
length: 10, length: 10,
animation_length: 0, animation_length: 0,
scroll_left: 0, scroll_left: 0,
scroll_top: 0,
head_width: 180, head_width: 180,
timecodes: [], timecodes: [],
animators: Timeline.animators, animators: Timeline.animators,
@ -657,6 +664,7 @@ onVueSetup(function() {
graph_editor_axis: 'x', graph_editor_axis: 'x',
graph_offset: 200, graph_offset: 200,
graph_size: 200, graph_size: 200,
graph_keyframe_values: new Map(),
channels: { channels: {
rotation: true, rotation: true,
@ -665,6 +673,11 @@ onVueSetup(function() {
hide_empty: false, hide_empty: false,
} }
}, },
computed: {
graph_editor_animator() {
return this.animators.find(animator => animator.selected && animator instanceof BoneAnimator);
}
},
methods: { methods: {
toggleAnimator(animator) { toggleAnimator(animator) {
animator.expanded = !animator.expanded; animator.expanded = !animator.expanded;
@ -686,34 +699,40 @@ onVueSetup(function() {
return points.join(' '); return points.join(' ');
}, },
getGraph() { getGraph() {
let ba = Animation.selected.getBoneAnimator(); let ba = this.graph_editor_animator;
let original_time = Timeline.time; let original_time = Timeline.time;
let step = Timeline.getStep() * this.size; let step = Timeline.getStep() * this.size;
step = Math.clamp(step, 1, 5) step = Math.clamp(step, 1, 5)
let timeline_offset = this.scroll_left - 9;
let timeline_offset_rounded = Math.round(timeline_offset/step)*step;
let clientWidth = this.$refs.graph_editor ? this.$refs.graph_editor.clientWidth : 400; let clientWidth = this.$refs.graph_editor ? this.$refs.graph_editor.clientWidth : 400;
let clientHeight = this.$refs.graph_editor ? this.$refs.graph_editor.clientHeight : 400; let clientHeight = this.$refs.timeline_body ? this.$refs.timeline_body.clientHeight : 400;
let points = []; let points = [];
let min = -1; let min = -1;
let max = 1; let max = 1;
for (let time = timeline_offset_rounded; time < timeline_offset + clientWidth; time += step) { for (let time = 0; time < clientWidth; time += step) {
Timeline.time = time / this.size; Timeline.time = time / this.size;
let value = ba.interpolate(this.graph_editor_channel, false, this.graph_editor_axis); let value = ba.interpolate(this.graph_editor_channel, false, this.graph_editor_axis);
points.push(value); points.push(value);
min = Math.min(min, value); min = Math.min(min, value);
max = Math.max(max, value); max = Math.max(max, value);
} }
Timeline.time = original_time;
let padding = 30 let padding = 30
this.graph_size = (clientHeight - 2*padding) / (max-min); this.graph_size = (clientHeight - 2*padding) / (max-min);
this.graph_offset = clientHeight - padding + (this.graph_size * min); this.graph_offset = clientHeight - padding + (this.graph_size * min);
this.graph_keyframe_values.clear();
ba[this.graph_editor_channel].forEach(kf => {
Timeline.time = kf.time;
let value = ba.interpolate(this.graph_editor_channel, false, this.graph_editor_axis);
kf.display_value = value;
})
Timeline.time = original_time;
let string = ''; let string = '';
points.forEach((value, i) => { points.forEach((value, i) => {
string += `${string.length ? 'L' : 'M'}${i*step - timeline_offset + timeline_offset_rounded} ${this.graph_offset - value * this.graph_size} ` string += `${string.length ? 'L' : 'M'}${i*step} ${this.graph_offset - value * this.graph_size} `
}) })
return string; return string;
@ -752,7 +771,7 @@ onVueSetup(function() {
</div> </div>
</div> </div>
</div> </div>
<div id="timeline_body"> <div id="timeline_body" ref="timeline_body">
<div id="timeline_body_inner" v-bind:style="{width: (size*length + head_width)+'px'}" @contextmenu.stop="Timeline.showMenu($event)"> <div id="timeline_body_inner" v-bind:style="{width: (size*length + head_width)+'px'}" @contextmenu.stop="Timeline.showMenu($event)">
<li v-for="animator in animators" class="animator" :class="{selected: animator.selected}" :uuid="animator.uuid" v-on:click="animator.select();"> <li v-for="animator in animators" class="animator" :class="{selected: animator.selected}" :uuid="animator.uuid" v-on:click="animator.select();">
<div class="animator_head_bar"> <div class="animator_head_bar">
@ -827,14 +846,28 @@ onVueSetup(function() {
<div id="timeline_empty_head" class="channel_head" v-bind:style="{left: scroll_left+'px', width: head_width+'px'}"> <div id="timeline_empty_head" class="channel_head" v-bind:style="{left: scroll_left+'px', width: head_width+'px'}">
</div> </div>
<div id="timeline_selector" class="selection_rectangle"></div> <div id="timeline_selector" class="selection_rectangle"></div>
<div id="timeline_graph_editor" ref="graph_editor" v-if="graph_editor_open" :style="{left: head_width + 'px', top: scroll_top + 'px'}">
<svg>
<path :d="\`M0 \${graph_offset} L10000 \${graph_offset}\`" style="stroke: var(--color-grid);"></path>
<path :d="getGraph()" :style="{stroke: 'var(--color-axis-' + graph_editor_axis + ')'}"></path>
</svg>
<keyframe
v-for="keyframe in graph_editor_animator[graph_editor_channel]"
v-bind:style="{left: (10 + keyframe.time * size) + 'px', top: (graph_offset - keyframe.display_value * graph_size - 8) + 'px', color: getColor(keyframe.color)}"
class="keyframe graph_keyframe"
v-bind:class="[keyframe.channel, keyframe.selected?'selected':'']"
v-bind:id="keyframe.uuid"
v-on:click.stop="keyframe.select($event)"
v-on:dblclick="keyframe.callPlayhead()"
:title="tl('timeline.'+keyframe.channel)"
@contextmenu.prevent="keyframe.showContextMenu($event)"
>
<i class="material-icons keyframe_icon_smaller" v-if="keyframe.interpolation == 'catmullrom'">lens</i>
<i class="material-icons" v-else>stop</i>
</keyframe>
</div>
</div> </div>
</div> </div>
<div id="timeline_graph_editor" ref="graph_editor" v-if="graph_editor_open" :style="{left: head_width + 'px'}">
<svg>
<path :d="\`M0 \${graph_offset} L10000 \${graph_offset}\`" style="stroke: var(--color-grid);"></path>
<path :d="getGraph()" :style="{stroke: 'var(--color-axis-' + graph_editor_axis + ')'}"></path>
</svg>
</div>
</div> </div>
` `
}) })