diff --git a/effects/effect.cpp b/effects/effect.cpp index a736844f5..a68b4d6c9 100644 --- a/effects/effect.cpp +++ b/effects/effect.cpp @@ -13,6 +13,7 @@ #include "ui/checkboxex.h" #include "project/clip.h" #include "panels/timeline.h" +#include "panels/effectcontrols.h" #include "effects/video/transformeffect.h" #include "effects/video/inverteffect.h" @@ -65,6 +66,10 @@ Effect* create_effect(int effect_id, Clip* c) { return NULL; } +double double_lerp(double a, double b, double t) { + return ((1.0 - t) * a) + (t * b); +} + Effect::Effect(Clip* c, int t, int i) : parent_clip(c), type(t), @@ -96,9 +101,11 @@ Effect::~Effect() { void Effect::copy_field_keyframes(Effect* e) { for (int i=0;irows.at(i)->keyframe_times = rows.at(i)->keyframe_times; + e->rows.at(i)->keyframe_types = rows.at(i)->keyframe_types; for (int j=0;jfieldCount();j++) { EffectField* field = row->field(j); - e->rows.at(i)->field(j)->keyframes = field->keyframes; + e->rows.at(i)->field(j)->keyframe_data = field->keyframe_data; } } } @@ -128,7 +135,8 @@ bool Effect::is_enabled() { } void Effect::load(QXmlStreamReader* stream) { - for (int i=0;ireadNext(); + /*for (int i=0;iatEnd() && !(stream->name() == "effect" && stream->isEndElement())) { stream->readNext(); @@ -138,27 +146,47 @@ void Effect::load(QXmlStreamReader* stream) { while (!stream->atEnd() && !(stream->name() == "effect" && stream->isEndElement())) { stream->readNext(); if (stream->name() == "field" && stream->isStartElement()) { - stream->readNext(); - switch (field->type) { - case EFFECT_FIELD_DOUBLE: - field->set_double_value(stream->text().toDouble()); - break; - case EFFECT_FIELD_COLOR: - field->set_color_value(QColor(stream->text().toString())); - break; - case EFFECT_FIELD_STRING: - field->set_string_value(stream->text().toString()); - break; - case EFFECT_FIELD_BOOL: - field->set_bool_value(stream->text() == "1"); - break; - case EFFECT_FIELD_COMBO: - field->set_combo_string(stream->text().toString()); - break; - case EFFECT_FIELD_FONT: - field->set_combo_string(stream->text().toString()); - break; - } + // read all keyframes + QVector keys; + while (!stream->atEnd() && (stream->name() == "field" && stream->isEndElement())) { + stream->readNext(); + if (stream->name() == "key" && stream->isStartElement()) { + EffectKeyframe kf; + for (int k=0;kattributes().size();k++) { + const QXmlStreamAttribute& attr = stream->attributes().at(k); + if (attr.name() == "frame") { + kf.frame = attr.value().toLong(); + } else if (attr.name() == "type") { + kf.type = attr.value().toInt(); + } else if (attr.name() == "value") { + switch (field->type) { + case EFFECT_FIELD_DOUBLE: + kf.data = attr.value().toDouble(); + break; + case EFFECT_FIELD_COLOR: + { + kf.data = QColor(attr.value().toString()); + } + break; + case EFFECT_FIELD_STRING: + kf.data = attr.value().toString(); + break; + case EFFECT_FIELD_BOOL: + kf.data = (stream->text() == "1"); + break; + case EFFECT_FIELD_COMBO: + kf.data = (stream->text().toInt()); + break; + case EFFECT_FIELD_FONT: + kf.data = (stream->text().toString()); + break; + } + } + } + keys.append(kf); + } + } + field->keyframes = keys; break; } } @@ -166,29 +194,32 @@ void Effect::load(QXmlStreamReader* stream) { break; } } - } - + }*/ } void Effect::save(QXmlStreamWriter* stream) { for (int i=0;iwriteStartElement("row"); + stream->writeStartElement("row"); + for (int j=0;jkeyframe_times.size();j++) { + stream->writeStartElement("key"); + stream->writeAttribute("frame", QString::number(row->keyframe_times.at(j))); + stream->writeAttribute("type", QString::number(row->keyframe_types.at(j))); + stream->writeEndElement(); // key + } for (int j=0;jfieldCount();j++) { EffectField* field = row->field(j); stream->writeStartElement("field"); - for (int k=0;kkeyframes.size();k++) { - const EffectKeyframe& key = field->keyframes.at(i); - stream->writeStartElement("key"); - stream->writeAttribute("frame", QString::number(key.frame)); - stream->writeAttribute("type", QString::number(key.type)); + for (int k=0;kkeyframe_data.size();k++) { + const QVariant& data = field->keyframe_data.at(k); + stream->writeStartElement("key"); switch (field->type) { - case EFFECT_FIELD_DOUBLE: stream->writeAttribute("value", QString::number(key.data.toDouble())); break; - case EFFECT_FIELD_COLOR: stream->writeAttribute("value", key.data.value().name()); break; - case EFFECT_FIELD_STRING: stream->writeAttribute("value", key.data.toString()); break; - case EFFECT_FIELD_BOOL: stream->writeAttribute("value", QString::number(key.data.toBool())); break; - case EFFECT_FIELD_COMBO: stream->writeAttribute("value", key.data.toString()); break; - case EFFECT_FIELD_FONT: stream->writeAttribute("value", key.data.toString()); break; + case EFFECT_FIELD_DOUBLE: stream->writeAttribute("value", QString::number(data.toDouble())); break; + case EFFECT_FIELD_COLOR: stream->writeAttribute("value", data.value().name()); break; + case EFFECT_FIELD_STRING: stream->writeAttribute("value", data.toString()); break; + case EFFECT_FIELD_BOOL: stream->writeAttribute("value", QString::number(data.toBool())); break; + case EFFECT_FIELD_COMBO: stream->writeAttribute("value", QString::number(data.toInt())); break; + case EFFECT_FIELD_FONT: stream->writeAttribute("value", data.toString()); break; } stream->writeEndElement(); } @@ -205,17 +236,28 @@ void Effect::process_audio(uint8_t*, int) {} /* Effect Row Definitions */ -EffectRow::EffectRow(Effect *parent, QGridLayout *uilayout, const QString &n, int row) : parent_effect(parent), ui(uilayout), name(n), ui_row(row) { - label = new QLabel(name); +EffectRow::EffectRow(Effect *parent, QGridLayout *uilayout, const QString &n, int row) : + parent_effect(parent), + keyframing(false), + ui(uilayout), + name(n), + ui_row(row) +{ + label = new QLabel(name); ui->addWidget(label, row, 0); + // DEBUG STARTS + QPushButton* nkf = new QPushButton(); + connect(nkf, SIGNAL(clicked(bool)), this, SLOT(set_keyframe_now())); + ui->addWidget(nkf, row, 5); + // DEBUG ENDS /*keyframe_enable = new CheckboxEx(); keyframe_enable->setToolTip("Enable Keyframes"); ui->addWidget(keyframe_enable, row, 5);*/ } EffectField* EffectRow::add_field(int type, int colspan) { - EffectField* field = new EffectField(type); + EffectField* field = new EffectField(this, type); fields.append(field); QWidget* element = field->get_ui_element(); ui->addWidget(element, ui_row, fields.size(), 1, colspan); @@ -228,6 +270,29 @@ EffectRow::~EffectRow() { } } +void EffectRow::set_keyframe_now() { + set_keyframe(panel_timeline->playhead-parent_effect->parent_clip->timeline_in+parent_effect->parent_clip->clip_in); +} + +void EffectRow::set_keyframe(long time) { + keyframing = true; + int index = -1; + for (int i=0;i 0) ? keyframe_types.last() : EFFECT_KEYFRAME_LINEAR); + } + for (int i=0;iset_keyframe_data(index); + } + panel_effect_controls->update_keyframes(); +} + EffectField* EffectRow::field(int i) { return fields.at(i); } @@ -238,25 +303,20 @@ int EffectRow::fieldCount() { /* Effect Field Definitions */ -EffectField::EffectField(int t) : type(t) { - // DEFAULT KEYFRAME - EffectKeyframe key; - key.frame = -1; - keyframes.append(key); - +EffectField::EffectField(EffectRow *parent, int t) : parent_row(parent), type(t) { switch (t) { case EFFECT_FIELD_DOUBLE: { LabelSlider* ls = new LabelSlider(); ui_element = ls; - connect(ls, SIGNAL(valueChanged()), this, SIGNAL(changed())); + connect(ls, SIGNAL(valueChanged()), this, SLOT(uiElementChange())); } break; case EFFECT_FIELD_COLOR: { ColorButton* cb = new ColorButton(); ui_element = cb; - connect(cb, SIGNAL(color_changed()), this, SIGNAL(changed())); + connect(cb, SIGNAL(color_changed()), this, SLOT(uiElementChange())); } break; case EFFECT_FIELD_STRING: @@ -264,14 +324,14 @@ EffectField::EffectField(int t) : type(t) { QTextEdit* edit = new QTextEdit(); edit->setUndoRedoEnabled(true); ui_element = edit; - connect(edit, SIGNAL(textChanged()), this, SIGNAL(changed())); + connect(edit, SIGNAL(textChanged()), this, SLOT(uiElementChange())); } break; case EFFECT_FIELD_BOOL: { CheckboxEx* cb = new CheckboxEx(); ui_element = cb; - connect(cb, SIGNAL(clicked(bool)), this, SIGNAL(changed())); + connect(cb, SIGNAL(clicked(bool)), this, SLOT(uiElementChange())); connect(cb, SIGNAL(toggled(bool)), this, SIGNAL(toggled(bool))); } break; @@ -279,73 +339,86 @@ EffectField::EffectField(int t) : type(t) { { ComboBoxEx* cb = new ComboBoxEx(); ui_element = cb; - connect(cb, SIGNAL(currentIndexChanged(int)), this, SIGNAL(changed())); + connect(cb, SIGNAL(currentIndexChanged(int)), this, SLOT(uiElementChange())); } break; case EFFECT_FIELD_FONT: { FontCombobox* fcb = new FontCombobox(); ui_element = fcb; - connect(fcb, SIGNAL(currentIndexChanged(int)), this, SIGNAL(changed())); + connect(fcb, SIGNAL(currentIndexChanged(int)), this, SLOT(uiElementChange())); } break; } } -bool EffectField::is_keyframed(long p) { - return keyframes.size() == 0 || p < 0; +QVariant EffectField::get_current_data() { + switch (type) { + case EFFECT_FIELD_DOUBLE: return static_cast(ui_element)->value(); break; + case EFFECT_FIELD_COLOR: return static_cast(ui_element)->get_color(); break; + case EFFECT_FIELD_STRING: return static_cast(ui_element)->toPlainText(); break; + case EFFECT_FIELD_BOOL: return static_cast(ui_element)->isChecked(); break; + case EFFECT_FIELD_COMBO: return static_cast(ui_element)->currentIndex(); break; + case EFFECT_FIELD_FONT: return static_cast(ui_element)->currentText(); break; + } + return QVariant(); } -QVariant EffectField::get_keyframe_data(long p) { - if (keyframes.size() == 1) { - return keyframes.at(0).data; - } +void EffectField::set_keyframe_data(int i) { + if (i == -1) { + keyframe_data.append(get_current_data()); + } else { + keyframe_data[i] = get_current_data(); + } +} - EffectKeyframe* before = NULL; - EffectKeyframe* after = NULL; - for (int i=1;iframe)) { - before = &keyframes[i]; - } else if (keyframes.at(i).frame > p && !(after != NULL && keyframes.at(i).frame >= after->frame)) { - after = &keyframes[i]; - } - } +void EffectField::get_keyframe_data(long frame, int* before, int* after, double* progress) { + int before_keyframe_index = -1; + int after_keyframe_index = -1; + long before_keyframe_time = LONG_MIN; + long after_keyframe_time = LONG_MAX; - // interpolate data - if (before != NULL && after != NULL) { - double progress = (p - before->frame)/(after->frame - before->frame); - switch (type) { - case EFFECT_FIELD_DOUBLE: - { - return lerp(before->data.toDouble(), after->data.toDouble(), progress); - } - break; - case EFFECT_FIELD_COLOR: - { - QColor before_color = before->data.value(); - QColor after_color = after->data.value(); - QColor interval_color = QColor( - lerp(before_color.red(), after_color.red(), progress), - lerp(before_color.green(), after_color.green(), progress), - lerp(before_color.blue(), after_color.blue(), progress) - ); - return QVariant(interval_color); - } - break; - case EFFECT_FIELD_STRING: - case EFFECT_FIELD_BOOL: - case EFFECT_FIELD_COMBO: - case EFFECT_FIELD_FONT: - return before->data; - break; - } - } + for (int i=0;ikeyframe_times.size();i++) { + long eval_keyframe_time = parent_row->keyframe_times.at(i); + if (eval_keyframe_time == frame) { + *before = i; + *after = i; + return; + } else if (eval_keyframe_time < frame && eval_keyframe_time > before_keyframe_time) { + before_keyframe_index = i; + before_keyframe_time = eval_keyframe_time; + } else if (eval_keyframe_time > frame && eval_keyframe_time < after_keyframe_time) { + after_keyframe_index = i; + after_keyframe_time = eval_keyframe_time; + } + } - // return pre or post data - if (before != NULL) return before->data; - if (after != NULL) return after->data; + if (type == EFFECT_FIELD_DOUBLE || type == EFFECT_FIELD_COLOR) { + if (before_keyframe_index > -1 && after_keyframe_index > -1) { + // interpolate + *before = before_keyframe_index; + *after = after_keyframe_index; + *progress = (double)(frame-before_keyframe_time)/(double)(after_keyframe_time-before_keyframe_time); + + // TODO routines for bezier - currently this is purely linear + } else if (before_keyframe_index > -1) { + *before = before_keyframe_index; + *after = before_keyframe_index; + } else if (after_keyframe_index > -1) { + *before = after_keyframe_index; + *after = after_keyframe_index; + } + } else { + *before = before_keyframe_index; + *after = before_keyframe_index; + } +} + +void EffectField::uiElementChange() { + if (parent_row->keyframing) { + parent_row->set_keyframe_now(); + } + emit changed(); } QWidget* EffectField::get_ui_element() { @@ -357,12 +430,28 @@ void EffectField::set_enabled(bool e) { } double EffectField::get_double_value(long p) { - return get_keyframe_data(p).toDouble(); + if (parent_row->keyframing) { + int before_keyframe; + int after_keyframe; + double progress; + get_keyframe_data(p, &before_keyframe, &after_keyframe, &progress); + double value; + if (before_keyframe == after_keyframe) { + value = keyframe_data.at(before_keyframe).toDouble(); + } else { + double before_data = keyframe_data.at(before_keyframe).toDouble(); + double after_data = keyframe_data.at(after_keyframe).toDouble(); + value = double_lerp(before_data, after_data, progress); + } + static_cast(ui_element)->set_value(value, false); + return value; + } else { + return static_cast(ui_element)->value(); + } } void EffectField::set_double_value(double v) { - - static_cast(ui_element)->set_value(v, false); + static_cast(ui_element)->set_value(v, false); } void EffectField::set_double_default_value(double v) { @@ -382,15 +471,15 @@ void EffectField::add_combo_item(const QString& name, const QVariant& data) { } int EffectField::get_combo_index(long p) { - return static_cast(ui_element)->currentIndex(); + return static_cast(ui_element)->currentIndex(); } const QVariant EffectField::get_combo_data(long p) { - return static_cast(ui_element)->currentData(); + return static_cast(ui_element)->currentData(); } const QString EffectField::get_combo_string(long p) { - return static_cast(ui_element)->itemText(get_keyframe_data(p).toInt()); + return static_cast(ui_element)->currentText(); } void EffectField::set_combo_index(int index) { @@ -426,6 +515,20 @@ void EffectField::set_font_name(const QString& s) { } QColor EffectField::get_color_value(long p) { + if (parent_row->keyframing) { + int before_keyframe; + int after_keyframe; + double progress; + get_keyframe_data(p, &before_keyframe, &after_keyframe, &progress); + if (before_keyframe == after_keyframe) { + return keyframe_data.at(before_keyframe).value(); + } else { + QColor before_data = keyframe_data.at(before_keyframe).value(); + QColor after_data = keyframe_data.at(after_keyframe).value(); + return QColor(lerp(before_data.red(), after_data.red(), progress), lerp(before_data.green(), after_data.green(), progress), lerp(before_data.blue(), after_data.blue(), progress)); + } + } + return static_cast(ui_element)->get_color(); } diff --git a/effects/effect.h b/effects/effect.h index d8d007372..45bc2a73f 100644 --- a/effects/effect.h +++ b/effects/effect.h @@ -1,4 +1,4 @@ -#ifndef EFFECT_H +#ifndef EFFECT_H #define EFFECT_H #include @@ -15,6 +15,7 @@ struct Clip; class QXmlStreamReader; class QXmlStreamWriter; class Effect; +class EffectRow; class CheckboxEx; enum VideoEffects { @@ -52,20 +53,18 @@ Effect* create_effect(int effect_id, Clip* c); #define EFFECT_KEYFRAME_HOLD 1 #define EFFECT_KEYFRAME_BEZIER 2 -struct EffectKeyframe { - long frame; - int type; - QVariant data; -}; - class EffectField : public QObject { Q_OBJECT public: - EffectField(int t); + EffectField(EffectRow* parent, int t); + EffectRow* parent_row; int type; - QVariant get_keyframe_data(long p); - bool is_keyframed(long p); + QVariant get_current_data(); + void set_keyframe_data(int i); + void get_keyframe_data(long frame, int* before, int *after, double* d); +// QVariant get_keyframe_data(long p); +// bool is_keyframed(long p); double get_double_value(long p); void set_double_value(double v); @@ -94,26 +93,35 @@ public: QWidget* get_ui_element(); void set_enabled(bool e); - QVector keyframes; + QVector keyframe_data; +private: + QWidget* ui_element; +private slots: + void uiElementChange(); signals: void changed(); void toggled(bool); -private: - QWidget* ui_element; }; -class EffectRow { +class EffectRow : public QObject { + Q_OBJECT public: EffectRow(Effect* parent, QGridLayout* uilayout, const QString& n, int row); ~EffectRow(); EffectField* add_field(int type, int colspan = 1); EffectField* field(int i); - int fieldCount(); + int fieldCount(); void set_keyframe(long time); void move_keyframe(long from, long to); void delete_keyframe(long time); QLabel* label; Effect* parent_effect; + + bool keyframing; + QVector keyframe_times; + QVector keyframe_types; +public slots: + void set_keyframe_now(); private: QGridLayout* ui; QString name; diff --git a/effects/video/inverteffect.cpp b/effects/video/inverteffect.cpp index 3a2aefd8f..581eda38d 100644 --- a/effects/video/inverteffect.cpp +++ b/effects/video/inverteffect.cpp @@ -29,9 +29,10 @@ Effect* InvertEffect::copy(Clip* c) { } void InvertEffect::process_gl(long p, QOpenGLShaderProgram& shader_prog, int* anchor_x, int* anchor_y) { - double value = amount_val->get_double_value(p)*0.01; - vert_shader.compileSourceCode("varying vec2 vTexCoord; void main() { vTexCoord = gl_MultiTexCoord0; gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; }"); - frag_shader.compileSourceCode("uniform sampler2D myTexture; varying vec2 vTexCoord; void main(void) { vec4 textureColor = texture2D(myTexture, vTexCoord); gl_FragColor = vec4(textureColor.r+((1.0-textureColor.r-textureColor.r)*" + QString::number(value) + "), textureColor.g+((1.0-textureColor.g-textureColor.g)*" + QString::number(value) + "), textureColor.b+((1.0-textureColor.b-textureColor.b)*" + QString::number(value) + "), 1); }"); + double value = amount_val->get_double_value(p)*0.01; + + vert_shader.compileSourceCode("varying vec2 vTexCoord;\nvoid main() {\n\tvTexCoord = gl_MultiTexCoord0.xy;\n\tgl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n}"); + frag_shader.compileSourceCode("uniform sampler2D myTexture;\nvarying vec2 vTexCoord;\nvoid main(void) {\n\tvec4 textureColor = texture2D(myTexture, vTexCoord);\n\tgl_FragColor = vec4(textureColor.r+((1.0-textureColor.r-textureColor.r)*" + QString::number(value, 'f', 2) + "), textureColor.g+((1.0-textureColor.g-textureColor.g)*" + QString::number(value, 'f', 2) + "), textureColor.b+((1.0-textureColor.b-textureColor.b)*" + QString::number(value, 'f', 2) + "), 1);\n}"); shader_prog.addShader(&vert_shader); shader_prog.addShader(&frag_shader); diff --git a/panels/effectcontrols.cpp b/panels/effectcontrols.cpp index c6788d424..83e15ebd7 100644 --- a/panels/effectcontrols.cpp +++ b/panels/effectcontrols.cpp @@ -144,12 +144,10 @@ void EffectControls::clear_effects(bool clear_cache) { while ((item = video_layout->takeAt(0))) { item->widget()->setParent(NULL); disconnect(static_cast(item->widget()), SIGNAL(deselect_others(QWidget*)), this, SLOT(deselect_all_effects(QWidget*))); - disconnect(static_cast(item->widget()), SIGNAL(visibleChanged()), ui->keyframeView, SLOT(reload())); } while ((item = audio_layout->takeAt(0))) { item->widget()->setParent(NULL); disconnect(static_cast(item->widget()), SIGNAL(deselect_others(QWidget*)), this, SLOT(deselect_all_effects(QWidget*))); - disconnect(static_cast(item->widget()), SIGNAL(visibleChanged()), ui->keyframeView, SLOT(reload())); } ui->vcontainer->setVisible(false); ui->acontainer->setVisible(false); @@ -189,7 +187,6 @@ void EffectControls::load_effects() { ui->acontainer->setVisible(true); } connect(container, SIGNAL(deselect_others(QWidget*)), this, SLOT(deselect_all_effects(QWidget*))); - connect(container, SIGNAL(visibleChanged()), ui->keyframeView, SLOT(reload())); } } if (selected_clips.size() > 0) { diff --git a/panels/effectcontrols.h b/panels/effectcontrols.h index 49accdd75..c2b871c99 100644 --- a/panels/effectcontrols.h +++ b/panels/effectcontrols.h @@ -36,11 +36,12 @@ public: void delete_effects(); bool is_focused(); void reload_clips(); - void update_keyframes(); void set_zoom(bool in); bool keyframe_focus(); double zoom; +public slots: + void update_keyframes(); private slots: void menu_select(QAction* q); void on_add_video_effect_button_clicked(); diff --git a/ui/keyframeview.cpp b/ui/keyframeview.cpp index c7c605a1b..3637a2738 100644 --- a/ui/keyframeview.cpp +++ b/ui/keyframeview.cpp @@ -29,13 +29,17 @@ void KeyframeView::paintEvent(QPaintEvent*) { Effect* e = effects.at(i); if (e->container->is_expanded()) { for (int j=0;jrow_count();j++) { - QLabel* label = e->row(j)->label; - QWidget* contents = e->container->contents; - long frame = 20; - int keyframe_y = label->y() + (label->height()>>1) + mapFrom(panel_effect_controls, contents->mapTo(panel_effect_controls, contents->pos())).y() - e->container->title_bar->height(); - draw_keyframe(p, getScreenPointFromFrame(panel_effect_controls->zoom, frame), keyframe_y, false); + EffectRow* row = e->row(j); - rows.append(e->row(j)); + QLabel* label = row->label; + QWidget* contents = e->container->contents; + + int keyframe_y = label->y() + (label->height()>>1) + mapFrom(panel_effect_controls, contents->mapTo(panel_effect_controls, contents->pos())).y() - e->container->title_bar->height(); + for (int k=0;kkeyframe_times.size();k++) { + draw_keyframe(p, getScreenPointFromFrame(panel_effect_controls->zoom, row->keyframe_times.at(k)), keyframe_y, false); + } + + rows.append(row); rowY.append(keyframe_y); } } @@ -65,7 +69,7 @@ void KeyframeView::mousePressEvent(QMouseEvent *event) { } void KeyframeView::mouseMoveEvent(QMouseEvent* event) { - unsetCursor(); + /*unsetCursor(); bool new_mo = false; for (int i=0;iy() > rowY.at(i)-KEYFRAME_SIZE-KEYFRAME_SIZE && event->y() < rowY.at(i)+KEYFRAME_SIZE+KEYFRAME_SIZE) { @@ -79,7 +83,7 @@ void KeyframeView::mouseMoveEvent(QMouseEvent* event) { if (new_mo || (new_mo != mouseover)) { update(); } - mouseover = new_mo; + mouseover = new_mo;*/ } void KeyframeView::mouseReleaseEvent(QMouseEvent* event) {