Animation properties dialog

This commit is contained in:
JannisX11 2020-09-14 16:26:17 +02:00
parent 713d5a5724
commit 4216ab904b
7 changed files with 325 additions and 221 deletions

View File

@ -295,7 +295,7 @@
.password_toggle {
display: inline-block;
margin-left: 4px;
margin-top: -1px;
margin-top: 4px;
width: 24px;
text-align: center;
vertical-align: text-bottom;
@ -534,6 +534,7 @@
label.name_space_left {
float: left;
min-width: 155px;
margin-top: 4px;
margin-left: 1px;
margin-right: 8px;
flex-shrink: 0;

View File

@ -104,8 +104,8 @@ class Animation {
if (this.length) ani_tag.animation_length = this.length;
if (this.override) ani_tag.override_previous_animation = true;
if (this.anim_time_update) ani_tag.anim_time_update = this.anim_time_update;
if (this.blend_weight) ani_tag.blend_weight = this.blend_weight;
if (this.anim_time_update) ani_tag.anim_time_update = this.anim_time_update.replace(/\n/g, '');
if (this.blend_weight) ani_tag.blend_weight = this.blend_weight.replace(/\n/g, '');
ani_tag.bones = {};
for (var uuid in this.animators) {
@ -421,7 +421,7 @@ class Animation {
})
}
if (timecodes.length > 1) {
for (var i = 10; i <= 80; i++) {
for (var i = 10; i <= 100; i++) {
let works = true;
for (var timecode of timecodes) {
let factor = (timecode * i) % 1;
@ -437,6 +437,84 @@ class Animation {
}
}
}
propertiesDialog() {
let vue_data = {
anim_time_update: this.anim_time_update,
blend_weight: this.blend_weight,
}
let dialog = new Dialog({
id: 'animation_properties',
title: this.name,
form_first: true,
form: {
name: {label: 'generic.name', value: this.name},
path: {
label: 'menu.animation.file',
value: this.path,
type: 'file',
extensions: ['json'],
filetype: 'JSON Animation',
condition: isApp
},
loop: {
label: 'menu.animation.loop',
type: 'select',
value: this.loop,
options: {
once: 'menu.animation.loop.once',
hold: 'menu.animation.loop.hold',
loop: 'menu.animation.loop.loop',
},
},
override: {label: 'menu.animation.override', type: 'checkbox', value: this.override},
snapping: {label: 'menu.animation.snapping', type: 'number', value: this.snapping, step: 1, min: 10, max: 100},
line: '_',
},
lines: [
`<div id="animation_properties_vue">
<label>${tl('menu.animation.anim_time_update')}</label>
<div class="dialog_bar">
<vue-prism-editor class="molang_input dark_bordered" v-model="anim_time_update" language="molang" :line-numbers="false" />
</div>
<label>${tl('menu.animation.blend_weight')}</label>
<div class="dialog_bar">
<vue-prism-editor class="molang_input dark_bordered" v-model="blend_weight" language="molang" :line-numbers="false" />
</div>
</div>`
],
onConfirm: form_data => {
console.log(form_data, vue_data)
dialog.hide()
if (
form_data.loop != this.loop
|| form_data.name != this.name
|| (isApp && form_data.path != this.path)
|| form_data.loop != this.loop
|| form_data.override != this.override
|| form_data.snapping != this.snapping
|| vue_data.anim_time_update != this.anim_time_update
|| vue_data.blend_weight != this.blend_weight
) {
Undo.initEdit({animations: [this]});
this.loop = form_data.loop;
this.name = form_data.name;
if (isApp) this.path = form_data.path;
this.loop = form_data.loop;
this.override = form_data.override;
this.snapping = Math.clamp(form_data.snapping, 10, 100);
this.anim_time_update = vue_data.anim_time_update;
this.blend_weight = vue_data.blend_weight;
Undo.finishEdit('edit animation properties');
}
}
})
dialog.show();
new Vue({
el: 'dialog#animation_properties #animation_properties_vue',
components: {VuePrismEditor},
data: vue_data
})
}
}
Animation.all = [];
Animation.prototype.menu = new Menu([
@ -445,15 +523,6 @@ class Animation {
{name: 'menu.animation.loop.hold', icon: animation => (animation.loop == 'hold' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('hold', true)}},
{name: 'menu.animation.loop.loop', icon: animation => (animation.loop == 'loop' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('loop', true)}},
]},
{name: 'menu.animation.override', icon: (a) => (a.override?'check_box':'check_box_outline_blank'), click: function(animation) {
animation.override = !animation.override
}},
{name: 'menu.animation.anim_time_update', icon: 'update', click: function(animation) {
animation.editUpdateVariable()
}},
{name: 'menu.animation.blend_weight', icon: 'fa-blender', click: function(animation) {
animation.editBlendWeight()
}},
'_',
{
name: 'menu.animation.save',
@ -464,8 +533,11 @@ class Animation {
}
},
'duplicate',
'rename',
'delete',
'_',
{name: 'menu.animation.properties', icon: 'list', click: function(animation) {
animation.propertiesDialog();
}}
])
new Property(Animation, 'boolean', 'saved', {default: true})
new Property(Animation, 'string', 'path')
@ -1076,8 +1148,12 @@ const Animator = {
path,
loop: a.loop && (a.loop == 'hold_on_last_frame' ? 'hold' : 'loop'),
override: a.override_previous_animation,
anim_time_update: a.anim_time_update,
blend_weight: a.blend_weight,
anim_time_update: (typeof a.anim_time_update == 'string'
? a.anim_time_update.replace(/;(?!$)/, ';\n')
: a.anim_time_update),
blend_weight: (typeof a.blend_weight == 'string'
? a.blend_weight.replace(/;(?!$)/, ';\n')
: a.blend_weight),
length: a.animation_length
}).add()
//Bones
@ -1223,7 +1299,7 @@ BARS.defineActions(function() {
click: function () {
var animation = new Animation({
name: 'animation.' + (Project.geometry_name||'model') + '.new'
}).add(true).rename()
}).add(true).propertiesDialog()
}
})

View File

@ -146,9 +146,9 @@ class Keyframe {
}
getArray() {
var arr = [
this.get('x'),
this.get('y'),
this.get('z'),
this.get('x').replace(/\n/g, ''),
this.get('y').replace(/\n/g, ''),
this.get('z').replace(/\n/g, ''),
]
return arr;
}

View File

@ -1,4 +1,207 @@
class Dialog {
(function() {
function buildForm(dialog) {
let jq_dialog = $(dialog.object)
for (var form_id in dialog.form) {
let data = dialog.form[form_id]
if (data === '_') {
jq_dialog.append('<hr />')
} else if (data && Condition(data.condition)) {
var bar = $(`<div class="dialog_bar form_bar form_bar_${form_id}"></div>`)
if (data.label) {
bar.append(`<label class="name_space_left" for="${form_id}">${tl(data.label)+(data.nocolon?'':':')}</label>`)
dialog.max_label_width = Math.max(getStringWidth(tl(data.label)), dialog.max_label_width)
}
switch (data.type) {
default:
bar.append(`<input class="dark_bordered half focusable_input" type="text" id="${form_id}" value="${data.value||''}" placeholder="${data.placeholder||''}" ${data.list ? `list="${dialog.id}_${form_id}_list"` : ''}>`)
if (data.list) {
let list = $(`<datalist id="${dialog.id}_${form_id}_list"></datalist>`)
for (let value of data.list) {
list.append(`<option value="${value}">`)
}
bar.append(list)
}
if (data.type == 'password') {
bar.append(`<div class="password_toggle" @click="setting.hidden = !setting.hidden;">
<i class="fas fa-eye-slash"></i>
</div>`)
let input = bar.find('input').attr('type', 'password')
let hidden = true;
let this_bar = bar;
this_bar.find('.password_toggle').click(e => {
hidden = !hidden;
input.attr('type', hidden ? 'password' : 'text');
this_bar.find('.password_toggle i')[0].className = hidden ? 'fas fa-eye-slash' : 'fas fa-eye';
})
}
break;
case 'textarea':
bar.append(`<textarea class="focusable_input" style="height: ${data.height||150}px;" id="${form_id}"></textarea>`)
break;
case 'select':
var el = $(`<div class="bar_select half"><select class="focusable_input" id="${form_id}"></select></div>`)
var sel = el.find('select')
for (var key in data.options) {
var name = tl(data.options[key])
sel.append(`<option id="${key}" ${(data.value === key || (data.default || data.value) === key) ? 'selected' : ''}>${name}</option>`)
}
bar.append(el)
break;
case 'radio':
var el = $(`<div class="half form_part_radio" id="${form_id}"></div>`)
for (var key in data.options) {
var name = tl(data.options[key])
el.append(`<div class="form_bar_radio">
<input type="radio" class="focusable_input" name="${form_id}_radio" id="${key}" ${(data.default || data.value) === key ? 'selected' : ''}>
<label for="${key}">${name}</label>
</div>`)
}
bar.append(el)
break;
case 'info':
data.text = marked(tl(data.text))
bar.append(`<p>${data.text}</p>`)
bar.addClass('small_text')
break;
case 'number':
bar.append(`<input class="dark_bordered half focusable_input" type="number" id="${form_id}"
value="${data.value||0}" min="${data.min}" max="${data.max}" step="${data.step||1}">`)
break;
case 'vector':
let group = $(`<div class="dialog_vector_group half"></div>`)
bar.append(group)
for (var i = 0; i < (data.dimensions || 3); i++) {
group.append(`<input class="dark_bordered focusable_input" type="number" id="${form_id}_${i}"
value="${data.value ? data.value[i]: 0}" step="${data.step||1}" min="${data.min}" max="${data.max}">`)
}
break;
case 'color':
if (!data.colorpicker) {
data.colorpicker = new ColorPicker({
id: 'cp_'+form_id,
name: tl(data.label),
label: false,
private: true
})
}
bar.append(data.colorpicker.getNode())
break;
case 'checkbox':
bar.append(`<input type="checkbox" class="focusable_input" id="${form_id}"${data.value ? ' checked' : ''}>`)
break;
case 'file':
case 'folder':
case 'save':
if (data.type == 'folder' && !isApp) break;
var input = $(`<input class="dark_bordered half" class="focusable_input" type="text" id="${form_id}" value="${data.value||''}" disabled>`);
bar.append(input);
bar.addClass('form_bar_file');
switch (data.type) {
case 'file': bar.append('<i class="material-icons">insert_drive_file</i>'); break;
case 'folder': bar.append('<i class="material-icons">folder</i>'); break;
case 'save': bar.append('<i class="material-icons">save</i>'); break;
}
let remove_button = $('<div class="tool" style="float: none; vertical-align: top;"><i class="material-icons">clear</i></div>');
bar.append(remove_button);
remove_button.on('click', e => {
e.stopPropagation();
data.value = '';
input.val('');
})
bar.on('click', e => {
function fileCB(files) {
data.value = files[0].path;
input.val(data.value);
}
switch (data.type) {
case 'file':
Blockbench.import({
resource_id: data.resource_id,
extensions: data.extensions,
type: data.filetype,
startpath: data.value
}, fileCB);
break;
case 'folder':
ElecDialogs.showOpenDialog(currentwindow, {
properties: ['openDirectory'],
defaultPath: data.value
}, (filePaths) => {
if (filePaths) fileCB([{ path: filePaths[0] }]);
})
break;
case 'save':
Blockbench.export({
resource_id: data.resource_id,
extensions: data.extensions,
type: data.filetype,
startpath: data.value,
custom_writer: () => {},
}, fileCB);
break;
}
})
}
if (data.readonly) {
bar.find('input').attr('readonly', 'readonly').removeClass('focusable_input')
}
jq_dialog.append(bar)
}
}
}
function buildLines(dialog) {
let jq_dialog = $(dialog.object)
dialog.lines.forEach(l => {
if (typeof l === 'object' && (l.label || l.widget)) {
var bar = $('<div class="dialog_bar"></div>')
if (l.label) {
bar.append('<label class="name_space_left">'+tl(l.label)+(l.nocolon?'':':')+'</label>')
dialog.max_label_width = Math.max(getStringWidth(tl(l.label)), dialog.max_label_width)
}
if (l.node) {
bar.append(l.node)
} else if (l.widget) {
var widget = l.widget
if (typeof l.widget === 'string') {
widget = BarItems[l.widget]
} else if (typeof l.widget === 'function') {
widget = l.widget()
}
bar.append(widget.getNode())
dialog.max_label_width = Math.max(getStringWidth(widget.name), dialog.max_label_width)
}
jq_dialog.append(bar)
} else {
jq_dialog.append(l)
}
})
}
window.Dialog = class Dialog {
constructor(options) {
this.id = options.id
this.title = options.title
@ -10,6 +213,7 @@ class Dialog {
this.singleButton = options.singleButton
this.buttons = options.buttons
this.fadeTime = options.fadeTime||0;
this.form_first = options.form_first;
this.confirmIndex = options.confirmIndex||0;
this.cancelIndex = options.cancelIndex !== undefined ? options.cancelIndex : 1;
@ -32,207 +236,18 @@ class Dialog {
var jq_dialog = $(`<dialog class="dialog paddinged" id="${this.id}"><div class="dialog_handle">${tl(this.title)}</div></dialog>`)
this.object = jq_dialog.get(0)
var max_label_width = 0;
if (this.lines) {
this.lines.forEach(l => {
if (typeof l === 'object' && (l.label || l.widget)) {
this.max_label_width = 0;
var bar = $('<div class="dialog_bar"></div>')
if (l.label) {
bar.append('<label class="name_space_left">'+tl(l.label)+(l.nocolon?'':':')+'</label>')
max_label_width = Math.max(getStringWidth(tl(l.label)), max_label_width)
}
if (l.node) {
bar.append(l.node)
} else if (l.widget) {
var widget = l.widget
if (typeof l.widget === 'string') {
widget = BarItems[l.widget]
} else if (typeof l.widget === 'function') {
widget = l.widget()
}
bar.append(widget.getNode())
max_label_width = Math.max(getStringWidth(widget.name), max_label_width)
}
jq_dialog.append(bar)
} else {
jq_dialog.append(l)
}
})
if (this.form_first) {
if (this.form) buildForm(this);
if (this.lines) buildLines(this);
} else {
if (this.lines) buildLines(this);
if (this.form) buildForm(this);
}
if (this.form) {
for (var form_id in this.form) {
let data = this.form[form_id]
if (data === '_') {
jq_dialog.append('<hr />')
} else if (data && Condition(data.condition)) {
var bar = $(`<div class="dialog_bar form_bar form_bar_${form_id}"></div>`)
if (data.label) {
bar.append(`<label class="name_space_left" for="${form_id}">${tl(data.label)+(data.nocolon?'':':')}</label>`)
max_label_width = Math.max(getStringWidth(tl(data.label)), max_label_width)
}
switch (data.type) {
default:
bar.append(`<input class="dark_bordered half focusable_input" type="text" id="${form_id}" value="${data.value||''}" placeholder="${data.placeholder||''}" ${data.list ? `list="${this.id}_${form_id}_list"` : ''}>`)
if (data.list) {
let list = $(`<datalist id="${this.id}_${form_id}_list"></datalist>`)
for (let value of data.list) {
list.append(`<option value="${value}">`)
}
bar.append(list)
}
if (data.type == 'password') {
bar.append(`<div class="password_toggle" @click="setting.hidden = !setting.hidden;">
<i class="fas fa-eye-slash"></i>
</div>`)
let input = bar.find('input').attr('type', 'password')
let hidden = true;
let this_bar = bar;
this_bar.find('.password_toggle').click(e => {
hidden = !hidden;
input.attr('type', hidden ? 'password' : 'text');
this_bar.find('.password_toggle i')[0].className = hidden ? 'fas fa-eye-slash' : 'fas fa-eye';
})
}
break;
case 'textarea':
bar.append(`<textarea class="focusable_input" style="height: ${data.height||150}px;" id="${form_id}"></textarea>`)
break;
case 'select':
var el = $(`<div class="bar_select half"><select class="focusable_input" id="${form_id}"></select></div>`)
var sel = el.find('select')
for (var key in data.options) {
var name = tl(data.options[key])
sel.append(`<option id="${key}" ${(data.value === key || data.default === key) ? 'selected' : ''}>${name}</option>`)
}
bar.append(el)
break;
case 'radio':
var el = $(`<div class="half form_part_radio" id="${form_id}"></div>`)
for (var key in data.options) {
var name = tl(data.options[key])
el.append(`<div class="form_bar_radio">
<input type="radio" class="focusable_input" name="${form_id}_radio" id="${key}" ${data.default === key ? 'selected' : ''}>
<label for="${key}">${name}</label>
</div>`)
}
bar.append(el)
break;
case 'info':
data.text = marked(tl(data.text))
bar.append(`<p>${data.text}</p>`)
bar.addClass('small_text')
break;
case 'number':
bar.append(`<input class="dark_bordered half focusable_input" type="number" id="${form_id}"
value="${data.value||0}" min="${data.min}" max="${data.max}" step="${data.step||1}">`)
break;
case 'vector':
let group = $(`<div class="dialog_vector_group half"></div>`)
bar.append(group)
for (var i = 0; i < (data.dimensions || 3); i++) {
group.append(`<input class="dark_bordered focusable_input" type="number" id="${form_id}_${i}"
value="${data.value ? data.value[i]: 0}" step="${data.step||1}" min="${data.min}" max="${data.max}">`)
}
break;
case 'color':
if (!data.colorpicker) {
data.colorpicker = new ColorPicker({
id: 'cp_'+form_id,
name: tl(data.label),
label: false,
private: true
})
}
bar.append(data.colorpicker.getNode())
break;
case 'checkbox':
bar.append(`<input type="checkbox" class="focusable_input" id="${form_id}"${data.value ? ' checked' : ''}>`)
break;
case 'file':
case 'folder':
case 'save':
if (data.type == 'folder' && !isApp) break;
var input = $(`<input class="dark_bordered half" class="focusable_input" type="text" id="${form_id}" value="${data.value||''}" disabled>`);
bar.append(input);
bar.addClass('form_bar_file');
switch (data.type) {
case 'file': bar.append('<i class="material-icons">insert_drive_file</i>'); break;
case 'folder': bar.append('<i class="material-icons">folder</i>'); break;
case 'save': bar.append('<i class="material-icons">save</i>'); break;
}
let remove_button = $('<div class="tool" style="float: none; vertical-align: top;"><i class="material-icons">clear</i></div>');
bar.append(remove_button);
remove_button.on('click', e => {
e.stopPropagation();
data.value = '';
input.val('');
})
bar.on('click', e => {
function fileCB(files) {
data.value = files[0].path;
input.val(data.value);
}
switch (data.type) {
case 'file':
Blockbench.import({
resource_id: data.resource_id,
extensions: data.extensions,
type: data.filetype,
startpath: data.value
}, fileCB);
break;
case 'folder':
ElecDialogs.showOpenDialog(currentwindow, {
properties: ['openDirectory'],
defaultPath: data.value
}, (filePaths) => {
if (filePaths) fileCB([{ path: filePaths[0] }]);
})
break;
case 'save':
Blockbench.export({
resource_id: data.resource_id,
extensions: data.extensions,
type: data.filetype,
startpath: data.value,
custom_writer: () => {},
}, fileCB);
break;
}
})
}
if (data.readonly) {
bar.find('input').attr('readonly', 'readonly').removeClass('focusable_input')
}
jq_dialog.append(bar)
}
}
}
if (max_label_width) {
document.styleSheets[0].insertRule('.dialog#'+this.id+' .dialog_bar label {width: '+(max_label_width+8)+'px}')
if (this.max_label_width) {
document.styleSheets[0].insertRule('.dialog#'+this.id+' .dialog_bar label {width: '+(this.max_label_width+8)+'px}')
}
if (this.buttons) {
@ -361,3 +376,7 @@ class Dialog {
if (bar.length) return bar;
}
}
})()

View File

@ -567,6 +567,11 @@ const MenuBar = {
'add_keyframe',
'add_marker',
'reverse_keyframes',
{name: 'menu.transform.flip', id: 'flip', condition: () => Timeline.selected.length, icon: 'flip', children: [
'flip_x',
'flip_y',
'flip_z'
]},
'delete',
'_',
'select_effect_animator',

View File

@ -358,7 +358,7 @@ function setupPanels() {
v-bind:anim_id="animation.uuid"
class="animation"
v-on:click.stop="animation.select()"
v-on:dblclick.stop="animation.rename()"
v-on:dblclick.stop="animation.propertiesDialog()"
:key="animation.uuid"
@contextmenu.prevent.stop="animation.showContextMenu($event)"
>

View File

@ -1092,7 +1092,7 @@
"menu.toolbar.edit": "Customize",
"menu.toolbar.reset": "Reset",
"menu.animation.loop": "Loop",
"menu.animation.loop": "Loop Mode",
"menu.animation.loop.once": "Play Once",
"menu.animation.loop.hold": "Hold On Last Frame",
"menu.animation.loop.loop": "Loop",
@ -1100,6 +1100,9 @@
"menu.animation.anim_time_update": "Anim Time Update Variable",
"menu.animation.blend_weight": "Blend Weight",
"menu.animation.save": "Save",
"menu.animation.properties": "Properties...",
"menu.animation.file": "File",
"menu.animation.snapping": "Snapping",
"menu.keyframe.quaternion": "Quaternion",