Skip to content

Commit 95eccaf

Browse files
authored
Merge pull request #932 from OpenShot/effect-sequencing
Allow Effects to be applied BEFORE or AFTER a clip's keyframes
2 parents b1104d8 + d5a7998 commit 95eccaf

35 files changed

+918
-1058
lines changed

src/Clip.cpp

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -440,21 +440,18 @@ std::shared_ptr<Frame> Clip::GetFrame(std::shared_ptr<openshot::Frame> backgroun
440440
// Apply waveform image (if any)
441441
apply_waveform(frame, background_frame);
442442

443-
// Apply local effects to the frame (if any)
444-
apply_effects(frame);
443+
// Apply effects BEFORE applying keyframes (if any local or global effects are used)
444+
apply_effects(frame, background_frame, options, true);
445445

446-
// Apply global timeline effects (i.e. transitions & masks... if any)
447-
if (timeline != NULL && options != NULL) {
448-
if (options->is_top_clip) {
449-
// Apply global timeline effects (only to top clip... if overlapping, pass in timeline frame number)
450-
Timeline* timeline_instance = static_cast<Timeline*>(timeline);
451-
frame = timeline_instance->apply_effects(frame, background_frame->number, Layer());
452-
}
453-
}
454-
455-
// Apply keyframe / transforms
446+
// Apply keyframe / transforms to current clip image
456447
apply_keyframes(frame, background_frame);
457448

449+
// Apply effects AFTER applying keyframes (if any local or global effects are used)
450+
apply_effects(frame, background_frame, options, false);
451+
452+
// Apply background canvas (i.e. flatten this image onto previous layer image)
453+
apply_background(frame, background_frame);
454+
458455
// Add final frame to cache
459456
final_cache.Add(frame);
460457

@@ -1202,16 +1199,41 @@ void Clip::RemoveEffect(EffectBase* effect)
12021199
final_cache.Clear();
12031200
}
12041201

1202+
// Apply background image to the current clip image (i.e. flatten this image onto previous layer)
1203+
void Clip::apply_background(std::shared_ptr<openshot::Frame> frame, std::shared_ptr<openshot::Frame> background_frame) {
1204+
// Add background canvas
1205+
std::shared_ptr<QImage> background_canvas = background_frame->GetImage();
1206+
QPainter painter(background_canvas.get());
1207+
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing, true);
1208+
1209+
// Composite a new layer onto the image
1210+
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
1211+
painter.drawImage(0, 0, *frame->GetImage());
1212+
painter.end();
1213+
1214+
// Add new QImage to frame
1215+
frame->AddImage(background_canvas);
1216+
}
1217+
12051218
// Apply effects to the source frame (if any)
1206-
void Clip::apply_effects(std::shared_ptr<Frame> frame)
1219+
void Clip::apply_effects(std::shared_ptr<Frame> frame, std::shared_ptr<Frame> background_frame, TimelineInfoStruct* options, bool before_keyframes)
12071220
{
1208-
// Find Effects at this position and layer
12091221
for (auto effect : effects)
12101222
{
12111223
// Apply the effect to this frame
1212-
frame = effect->GetFrame(frame, frame->number);
1224+
if (effect->info.apply_before_clip && before_keyframes) {
1225+
effect->GetFrame(frame, frame->number);
1226+
} else if (!effect->info.apply_before_clip && !before_keyframes) {
1227+
effect->GetFrame(frame, frame->number);
1228+
}
1229+
}
12131230

1214-
} // end effect loop
1231+
if (timeline != NULL && options != NULL) {
1232+
// Apply global timeline effects (i.e. transitions & masks... if any)
1233+
Timeline* timeline_instance = static_cast<Timeline*>(timeline);
1234+
options->is_before_clip_keyframes = before_keyframes;
1235+
timeline_instance->apply_effects(frame, background_frame->number, Layer(), options);
1236+
}
12151237
}
12161238

12171239
// Compare 2 floating point numbers for equality
@@ -1228,20 +1250,16 @@ void Clip::apply_keyframes(std::shared_ptr<Frame> frame, std::shared_ptr<Frame>
12281250
return;
12291251
}
12301252

1231-
// Get image from clip
1253+
// Get image from clip, and create transparent background image
12321254
std::shared_ptr<QImage> source_image = frame->GetImage();
1233-
std::shared_ptr<QImage> background_canvas = background_frame->GetImage();
1255+
std::shared_ptr<QImage> background_canvas = std::make_shared<QImage>(background_frame->GetImage()->width(),
1256+
background_frame->GetImage()->height(),
1257+
QImage::Format_RGBA8888_Premultiplied);
1258+
background_canvas->fill(QColor(Qt::transparent));
12341259

12351260
// Get transform from clip's keyframes
12361261
QTransform transform = get_transform(frame, background_canvas->width(), background_canvas->height());
12371262

1238-
// Debug output
1239-
ZmqLogger::Instance()->AppendDebugMethod(
1240-
"Clip::ApplyKeyframes (Transform: Composite Image Layer: Prepare)",
1241-
"frame->number", frame->number,
1242-
"background_canvas->width()", background_canvas->width(),
1243-
"background_canvas->height()", background_canvas->height());
1244-
12451263
// Load timeline's new frame image into a QPainter
12461264
QPainter painter(background_canvas.get());
12471265
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing, true);

src/Clip.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,11 @@ namespace openshot {
127127
/// Adjust frame number minimum value
128128
int64_t adjust_frame_number_minimum(int64_t frame_number);
129129

130+
/// Apply background image to the current clip image (i.e. flatten this image onto previous layer)
131+
void apply_background(std::shared_ptr<openshot::Frame> frame, std::shared_ptr<openshot::Frame> background_frame);
132+
130133
/// Apply effects to the source frame (if any)
131-
void apply_effects(std::shared_ptr<openshot::Frame> frame);
134+
void apply_effects(std::shared_ptr<openshot::Frame> frame, std::shared_ptr<openshot::Frame> background_frame, TimelineInfoStruct* options, bool before_keyframes);
132135

133136
/// Apply keyframes to an openshot::Frame and use an existing background frame (if any)
134137
void apply_keyframes(std::shared_ptr<Frame> frame, std::shared_ptr<Frame> background_frame);

src/EffectBase.cpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ void EffectBase::InitEffectInfo()
3030
End(0.0);
3131
Order(0);
3232
ParentClip(NULL);
33-
3433
parentEffect = NULL;
3534

3635
info.has_video = false;
@@ -39,6 +38,7 @@ void EffectBase::InitEffectInfo()
3938
info.name = "";
4039
info.description = "";
4140
info.parent_effect_id = "";
41+
info.apply_before_clip = true;
4242
}
4343

4444
// Display file information
@@ -51,6 +51,8 @@ void EffectBase::DisplayInfo(std::ostream* out) {
5151
*out << "--> Description: " << info.description << std::endl;
5252
*out << "--> Has Video: " << info.has_video << std::endl;
5353
*out << "--> Has Audio: " << info.has_audio << std::endl;
54+
*out << "--> Apply Before Clip Keyframes: " << info.apply_before_clip << std::endl;
55+
*out << "--> Order: " << order << std::endl;
5456
*out << "----------------------------" << std::endl;
5557
}
5658

@@ -85,6 +87,7 @@ Json::Value EffectBase::JsonValue() const {
8587
root["has_video"] = info.has_video;
8688
root["has_audio"] = info.has_audio;
8789
root["has_tracked_object"] = info.has_tracked_object;
90+
root["apply_before_clip"] = info.apply_before_clip;
8891
root["order"] = Order();
8992

9093
// return JsonValue
@@ -145,6 +148,9 @@ void EffectBase::SetJsonValue(const Json::Value root) {
145148
if (!my_root["order"].isNull())
146149
Order(my_root["order"].asInt());
147150

151+
if (!my_root["apply_before_clip"].isNull())
152+
info.apply_before_clip = my_root["apply_before_clip"].asBool();
153+
148154
if (!my_root["parent_effect_id"].isNull()){
149155
info.parent_effect_id = my_root["parent_effect_id"].asString();
150156
if (info.parent_effect_id.size() > 0 && info.parent_effect_id != "" && parentEffect == NULL)
@@ -169,6 +175,28 @@ Json::Value EffectBase::JsonInfo() const {
169175
return root;
170176
}
171177

178+
// Get all properties for a specific frame
179+
Json::Value EffectBase::BasePropertiesJSON(int64_t requested_frame) const {
180+
// Generate JSON properties list
181+
Json::Value root;
182+
root["id"] = add_property_json("ID", 0.0, "string", Id(), NULL, -1, -1, true, requested_frame);
183+
root["position"] = add_property_json("Position", Position(), "float", "", NULL, 0, 30 * 60 * 60 * 48, false, requested_frame);
184+
root["layer"] = add_property_json("Track", Layer(), "int", "", NULL, 0, 20, false, requested_frame);
185+
root["start"] = add_property_json("Start", Start(), "float", "", NULL, 0, 30 * 60 * 60 * 48, false, requested_frame);
186+
root["end"] = add_property_json("End", End(), "float", "", NULL, 0, 30 * 60 * 60 * 48, false, requested_frame);
187+
root["duration"] = add_property_json("Duration", Duration(), "float", "", NULL, 0, 30 * 60 * 60 * 48, true, requested_frame);
188+
189+
// Add replace_image choices (dropdown style)
190+
root["apply_before_clip"] = add_property_json("Apply Before Clip Keyframes", info.apply_before_clip, "int", "", NULL, 0, 1, false, requested_frame);
191+
root["apply_before_clip"]["choices"].append(add_property_choice_json("Yes", true, info.apply_before_clip));
192+
root["apply_before_clip"]["choices"].append(add_property_choice_json("No", false, info.apply_before_clip));
193+
194+
// Set the parent effect which properties this effect will inherit
195+
root["parent_effect_id"] = add_property_json("Parent", 0.0, "string", info.parent_effect_id, NULL, -1, -1, false, requested_frame);
196+
197+
return root;
198+
}
199+
172200
/// Parent clip object of this reader (which can be unparented and NULL)
173201
openshot::ClipBase* EffectBase::ParentClip() {
174202
return clip;

src/EffectBase.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ namespace openshot
4040
bool has_video; ///< Determines if this effect manipulates the image of a frame
4141
bool has_audio; ///< Determines if this effect manipulates the audio of a frame
4242
bool has_tracked_object; ///< Determines if this effect track objects through the clip
43+
bool apply_before_clip; ///< Apply effect before we evaluate the clip's keyframes
4344
};
4445

4546
/**
@@ -58,7 +59,6 @@ namespace openshot
5859
openshot::ClipBase* clip; ///< Pointer to the parent clip instance (if any)
5960

6061
public:
61-
6262
/// Parent effect (which properties will set this effect properties)
6363
EffectBase* parentEffect;
6464

@@ -106,7 +106,11 @@ namespace openshot
106106
return;
107107
};
108108

109-
Json::Value JsonInfo() const; ///< Generate JSON object of meta data / info
109+
/// Generate JSON object of meta data / info
110+
Json::Value JsonInfo() const;
111+
112+
/// Generate JSON object of base properties (recommended to be used by all effects)
113+
Json::Value BasePropertiesJSON(int64_t requested_frame) const;
110114

111115
/// Get the order that this effect should be executed.
112116
int Order() const { return order; }

src/Timeline.cpp

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ double Timeline::calculate_time(int64_t number, Fraction rate)
523523
}
524524

525525
// Apply effects to the source frame (if any)
526-
std::shared_ptr<Frame> Timeline::apply_effects(std::shared_ptr<Frame> frame, int64_t timeline_frame_number, int layer)
526+
std::shared_ptr<Frame> Timeline::apply_effects(std::shared_ptr<Frame> frame, int64_t timeline_frame_number, int layer, TimelineInfoStruct* options)
527527
{
528528
// Debug output
529529
ZmqLogger::Instance()->AppendDebugMethod(
@@ -541,21 +541,19 @@ std::shared_ptr<Frame> Timeline::apply_effects(std::shared_ptr<Frame> frame, int
541541

542542
bool does_effect_intersect = (effect_start_position <= timeline_frame_number && effect_end_position >= timeline_frame_number && effect->Layer() == layer);
543543

544-
// Debug output
545-
ZmqLogger::Instance()->AppendDebugMethod(
546-
"Timeline::apply_effects (Does effect intersect)",
547-
"effect->Position()", effect->Position(),
548-
"does_effect_intersect", does_effect_intersect,
549-
"timeline_frame_number", timeline_frame_number,
550-
"layer", layer);
551-
552544
// Clip is visible
553545
if (does_effect_intersect)
554546
{
555547
// Determine the frame needed for this clip (based on the position on the timeline)
556548
long effect_start_frame = (effect->Start() * info.fps.ToDouble()) + 1;
557549
long effect_frame_number = timeline_frame_number - effect_start_position + effect_start_frame;
558550

551+
if (!options->is_top_clip)
552+
continue; // skip effect, if overlapped/covered by another clip on same layer
553+
554+
if (options->is_before_clip_keyframes != effect->info.apply_before_clip)
555+
continue; // skip effect, if this filter does not match
556+
559557
// Debug output
560558
ZmqLogger::Instance()->AppendDebugMethod(
561559
"Timeline::apply_effects (Process Effect)",
@@ -615,6 +613,7 @@ void Timeline::add_layer(std::shared_ptr<Frame> new_frame, Clip* source_clip, in
615613
// Create timeline options (with details about this current frame request)
616614
TimelineInfoStruct* options = new TimelineInfoStruct();
617615
options->is_top_clip = is_top_clip;
616+
options->is_before_clip_keyframes = true;
618617

619618
// Get the clip's frame, composited on top of the current timeline frame
620619
std::shared_ptr<Frame> source_frame;

src/Timeline.h

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,13 @@ namespace openshot {
6868
/// the Clip with the highest end-frame number using std::max_element
6969
struct CompareClipEndFrames {
7070
bool operator()(const openshot::Clip* lhs, const openshot::Clip* rhs) {
71-
return (lhs->Position() + lhs->Duration())
72-
<= (rhs->Position() + rhs->Duration());
71+
return (lhs->Position() + lhs->Duration()) <= (rhs->Position() + rhs->Duration());
7372
}};
7473

7574
/// Like CompareClipEndFrames, but for effects
7675
struct CompareEffectEndFrames {
7776
bool operator()(const openshot::EffectBase* lhs, const openshot::EffectBase* rhs) {
78-
return (lhs->Position() + lhs->Duration())
79-
<= (rhs->Position() + rhs->Duration());
77+
return (lhs->Position() + lhs->Duration()) <= (rhs->Position() + rhs->Duration());
8078
}};
8179

8280
/**
@@ -231,7 +229,7 @@ namespace openshot {
231229
/// @param convert_absolute_paths Should all paths be converted to absolute paths (relative to the location of projectPath)
232230
Timeline(const std::string& projectPath, bool convert_absolute_paths);
233231

234-
virtual ~Timeline();
232+
virtual ~Timeline();
235233

236234
/// Add to the tracked_objects map a pointer to a tracked object (TrackedObjectBBox)
237235
void AddTrackedObject(std::shared_ptr<openshot::TrackedObjectBase> trackedObject);
@@ -240,9 +238,9 @@ namespace openshot {
240238
/// Return the ID's of the tracked objects as a list of strings
241239
std::list<std::string> GetTrackedObjectsIds() const;
242240
/// Return the trackedObject's properties as a JSON string
243-
#ifdef USE_OPENCV
241+
#ifdef USE_OPENCV
244242
std::string GetTrackedObjectValues(std::string id, int64_t frame_number) const;
245-
#endif
243+
#endif
246244

247245
/// @brief Add an openshot::Clip to the timeline
248246
/// @param clip Add an openshot::Clip to the timeline. A clip can contain any type of Reader.
@@ -252,8 +250,8 @@ namespace openshot {
252250
/// @param effect Add an effect to the timeline. An effect can modify the audio or video of an openshot::Frame.
253251
void AddEffect(openshot::EffectBase* effect);
254252

255-
/// Apply global/timeline effects to the source frame (if any)
256-
std::shared_ptr<openshot::Frame> apply_effects(std::shared_ptr<openshot::Frame> frame, int64_t timeline_frame_number, int layer);
253+
/// Apply global/timeline effects to the source frame (if any)
254+
std::shared_ptr<openshot::Frame> apply_effects(std::shared_ptr<openshot::Frame> frame, int64_t timeline_frame_number, int layer, TimelineInfoStruct* options);
257255

258256
/// Apply the timeline's framerate and samplerate to all clips
259257
void ApplyMapperToClips();
@@ -266,7 +264,7 @@ namespace openshot {
266264

267265
/// Clear all clips, effects, and frame mappers from timeline (and free memory)
268266
void Clear();
269-
267+
270268
/// Clear all cache for this timeline instance, including all clips' cache
271269
/// @param deep If True, clear all FrameMappers and nested Readers (QtImageReader, FFmpegReader, etc...)
272270
void ClearAllCache(bool deep=false);

src/TimelineBase.h

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@
1818

1919

2020
namespace openshot {
21-
// Forward decl
22-
class Clip;
23-
24-
/**
25-
* @brief This struct contains info about the current Timeline clip instance
26-
*
27-
* When the Timeline requests an openshot::Frame instance from a Clip, it passes
28-
* this struct along, with some additional details from the Timeline, such as if this clip is
29-
* above or below overlapping clips, etc... This info can help determine if a Clip should apply
30-
* global effects from the Timeline, such as a global Transition/Mask effect.
31-
*/
32-
struct TimelineInfoStruct
33-
{
34-
bool is_top_clip; ///< Is clip on top (if overlapping another clip)
35-
};
21+
// Forward decl
22+
class Clip;
23+
24+
/**
25+
* @brief This struct contains info about the current Timeline clip instance
26+
*
27+
* When the Timeline requests an openshot::Frame instance from a Clip, it passes
28+
* this struct along, with some additional details from the Timeline, such as if this clip is
29+
* above or below overlapping clips, etc... This info can help determine if a Clip should apply
30+
* global effects from the Timeline, such as a global Transition/Mask effect.
31+
*/
32+
struct TimelineInfoStruct
33+
{
34+
bool is_top_clip; ///< Is clip on top (if overlapping another clip)
35+
bool is_before_clip_keyframes; ///< Is this before clip keyframes are applied
36+
};
3637

3738
/**
3839
* @brief This class represents a timeline (used for building generic timeline implementations)

0 commit comments

Comments
 (0)