Hi,
Thank you for asking this question because it was a nice coding challenge!
Here is my solution :
-
Let’s suppose you have a list of movies, it’s a Sequence
-
Each Movie
has it’s own duration
-
If you were to draw a timeline of the sequence with every movie, you would have this :
-
Now we want to have linear crossfade which mean we can represent it like this (centered around the cut) :
-
When we want to display the sequence at a certain time, we need to first loop through the list of movies and find the one we are currently in, we can do this with this code :
// We start at 0
float previousCut = 0;
// Loop through movies
for (Movie movie : movies) {
// The next cut
float nextCut = previousCut + movie.duration;
if (currentTime < nextCut) {
// We display the movie
// Then break the loop
break;
}
// It's not this movie, increase the cut
previousCut += movie.duration
}
-
Then we need to handle two cases, when the timeline cursor is at the end of the clip and we fade out, we need to crossfade is this condition is true
:
-
We do the same for when the cursor is at the beginning of a clip and we fade in :
-
We then simply linearly interpolate (with map()
) the alpha value for both movies (the current one and the previous/next one for fade in/out). Be careful to switch the order by which you display both movies because it’s going to glitch as soon as the cursor pass after a cut.
These two methods compute the alpha value by passing the distance from the beginning of the fade :
// Compute the alpha value when fading out
float computeFadeOut(float distFromCut) {
return map(distFromCut, 0, fadeDuration, 255, 0);
}
// Same for fade in but reverse
float computeFadeIn(float distFromCut) {
return map(distFromCut, 0, fadeDuration, 0, 255);
}
Here you have a complete sketch with the above principle :
Sequence sequence;
void setup() {
size(1000, 600);
// Create the sequence
sequence = new Sequence(5000);
// Initialize random clips in milliseconds
sequence.initializeRandom(5, 5000, 10000);
}
void draw() {
// Reset the background
background(0);
// Display the sequence output
sequence.displayPlayback();
// Display the timeline
sequence.displayTimeline(width / 2, height - 50, width - 50, 50);
// Stop when finished
if (millis() > sequence.getTotalDuration()) noLoop();
}
/*
* The Movie class with a duration and a color
*/
class Movie {
float duration;
color col;
Movie(float duration, color col) {
this.duration = duration;
this.col = col;
}
// Initialize the movie with a black background
Movie(float duration) {
this(duration, color(0));
}
void display(float x, float y, float sizeX, float sizeY, float alpha) {
fill(col, alpha);
rect(x, y, sizeX, sizeY);
}
int getDurationSeconds() {
return floor(duration / 1000);
}
void randomizeColor() {
col = color(random(255), random(255), random(255));
}
// Display the movie on the whole window
void fullScreen(float alpha) {
display(0, 0, width, height, alpha);
}
}
/*
* The Sequence class is holding a list of movies and
* a fade duration for crossfading between movies
*/
class Sequence {
ArrayList<Movie> movies;
float fadeDuration;
Sequence(float fadeDuration) {
this.movies = new ArrayList<Movie>();
this.fadeDuration = fadeDuration;
}
// Randomly create movies with different durations
void initializeRandom(int n, float minDuration, float maxDuration) {
for (int i = 0; i < n; i++) {
Movie movie = new Movie(random(minDuration, maxDuration));
movie.randomizeColor();
movies.add(movie);
}
}
// Return the total duration of the sequence
float getTotalDuration() {
float total = 0;
for (Movie movie : movies) total += movie.duration;
return total;
}
// Compute the alpha value when fading out
float computeFadeOut(float distFromCut) {
return map(distFromCut, 0, fadeDuration, 255, 0);
}
// Same for fade in but reverse
float computeFadeIn(float distFromCut) {
return map(distFromCut, 0, fadeDuration, 0, 255);
}
// The display method where we do the crossfade
void displayPlayback() {
noStroke();
float previousCut = 0;
float currentTime = millis();
for (int i = 0; i < movies.size(); i++) {
Movie movie = movies.get(i);
// The timestamp of the next movie
float nextCut = previousCut + movie.duration;
// If true this is the current movie
if (currentTime < nextCut) {
// Test if we need to crossfade
if (nextCut - currentTime < fadeDuration / 2) {
float distFromCut = currentTime - (nextCut - fadeDuration / 2);
movie.fullScreen(computeFadeOut(distFromCut));
if (i < movies.size() - 1) {
movies.get(i + 1).fullScreen(computeFadeIn(distFromCut));
}
} else if (currentTime - previousCut < fadeDuration / 2) { // After the previous clip
float distFromCut = currentTime - (previousCut - fadeDuration / 2);
if (i > 0) {
movies.get(i - 1).fullScreen(computeFadeOut(distFromCut));
}
movie.fullScreen(computeFadeIn(distFromCut));
} else {
// We are not crossfading
movie.fullScreen(255);
}
// Display time stamp
fill(0);
textAlign(CENTER, CENTER);
textSize(45);
text((previousCut + movie.duration - millis()) / 1000, width / 2, height / 2);
// Be break to stop the loop
break;
}
// Each time we add the movie length
previousCut += floor(movie.duration);
}
}
void displayTimeline(float centerX, float centerY, float hSize, float vSize) {
float totalDuration = getTotalDuration();
float leftX = centerX - (hSize / 2);
float incrX = leftX;
float topY = centerY - vSize / 2;
float downY = centerY + vSize / 2;
float fadeSize = map(fadeDuration, 0, totalDuration, 0, hSize);
for (int i = 0; i < movies.size(); i++) {
float clipWidth = map(movies.get(i).duration, 0, totalDuration, 0, hSize);
// Display clip
stroke(0);
strokeWeight(3);
movies.get(i).display(incrX, topY, clipWidth, vSize, 200);
// Display cross fade
stroke(255, 0, 0);
strokeWeight(2);
noFill();
if (i > 0) {
// Fade in
line(incrX - fadeSize / 2, downY, incrX + fadeSize / 2, topY);
// Fade out
line(incrX - fadeSize / 2, topY, incrX + fadeSize / 2, downY);
} else {
// Secial case for the first fade in
line(incrX, centerY, incrX + fadeSize / 2, topY);
}
// Also for the last one
if (i == movies.size() - 1) {
line(incrX + clipWidth - fadeSize / 2, topY, incrX + clipWidth, centerY);
}
// Display duration
fill(0);
noStroke();
textSize(20);
text(movies.get(i).getDurationSeconds(), incrX + clipWidth / 2, topY - 20);
// Add the size of the drawn clip
incrX += clipWidth;
}
// Display the time cursor
float cursorX = map(millis(), 0, totalDuration, leftX, leftX + hSize);
stroke(255, 0, 0);
strokeWeight(4);
line(cursorX, topY, cursorX, downY);
// Draw triangle on top
fill(255, 0, 0);
triangle(cursorX, topY + 5, cursorX - 5, topY - 5, cursorX + 5, topY - 5);
}
}
Note : I put comments to explain it and don’t feel lost, it’s just that I made the whole drawing part for the timeline and it’s not the real core of the problem, you should look at the void displayPlayback()
method first.
I also didn’t use the Processing movie library but rather simulate a movie with a solid color background. It should be easy to adapt
Hope it was clear!