Moving Pictures with Sinatra, Part III

Share this article

mp_sinatra

In part I we made a little app to read jpegs from a directory. In part II we created an uploader so we can now add new pictures. All that’s left is to do now is make the slideshow.

Let’s Animate the Pictures

In this article Craig goes over CSS animation. Let’s leverage the keyframes technique described there to change the opacity of the images. Start with the opacity at zero, and go to one, to show the image in all it’s glory.

We want a picture to display for roughly five seconds and loop through them forever. Here’s some example code.

img#animation1 {
  -webkit-animation:cycleone 5s infinite;
}

@-webkit-keyframes cycleone {
  0%   {opacity:0} /* not visible */
  10%  {opacity:1} /* visible     */
  90%  {opacity:1} /* visible     */
  100% {opacity:0} /* not visible */
}

There are 100 keyframes in the duration of the animation. Divide that by how long we want to display the picture and we’ll know how many keyframes we need per second.

100 keyframes / 5 seconds = 20 keyframes  
20 keyframes = 1 second

In the example code above, there’s a half second fade in, the image is displayed for four seconds, and then a half second fade out.

What happens when there are two, or seven, pictures?

Time for Math

If we want 5 seconds for each picture and we have 2 pictures, how would you find the correct keyframe percentages?

We can divide the original keyframe percentages by the number of pictures to find what keyframe percentages are needed to set the first picture

* 0/2    => 0  
* 10/2   => 5  
* 90/2   => 45  
* 100/2  => 50

On the second picture we need to start where we previously left off.

* 0/2 + 50    => 50  
* 10/2 + 50   => 55  
* 90/2 + 50   => 95  
* 100/2 + 50  => 100

Time to Get Real.

Let’s make a new file to programmatically figure this out. I’ll call mine slideshow.rb

$ touch slideshow.rb

We need to write the CSS for each img tag. Each picture will have an ID that can use in a loop to write out the CSS.
Find out the total duration of the slide show. Two pictures and each picture for five seconds.

puts 2 * 5

If you run that you’ll see the answer. 10. Duh. Yes that’s the total duration but that doesn’t help much. What about this code?

slideshow.rb

PICTURES = 2  

for i in 1..PICTURES  
  puts "img\#animation" + i.to_s + " {"  
  puts "    -webkit-animation:cycle" + i.to_s + " " + ( 2 * 5 ).to_s + "s infinite;"  
  puts "}"  
end

Run it and see what we get.

$ ruby slideshow.rb  
img#animation1 {  
    -webkit-animation:cycle1 10s infinite;  
}  
img#animation2 {  
    -webkit-animation:cycle2 10s infinite;  
}

Cool. Now all we have to do is write the animation rules. Remember, we need to add the last number of the first cycle to the second cycle

slideshow.rb

PICTURES = 2

@offset = 0

for i in 1..PICTURES
  puts "img\#animation" + i.to_s + " {"
  puts "    -webkit-animation:cycle" + i.to_s + " " + ( 2 * 5 ).to_s + "s infinite;"
  puts "}"

  puts "@-webkit-keyframes cycle" + i.to_s + " {"
  puts " #{@offset}% {opacity:0}"
  puts "  " + (10 / PICTURES + @offset).to_s + "% {opacity:1}"
  puts "  " + (90 / PICTURES + @offset).to_s + "% {opacity:1}"
  puts "  " + (100 / PICTURES + @offset).to_s + "% {opacity:0}"
  puts "}"

  #reset offset
  @offset = 100 / PICTURES + @offset
end

We’ve added a variable to handle the offset for the next cycle.

Go ahead and run that.

$ ruby slideshow.rb  
img#animation1 {
    -webkit-animation:cycle1 10s infinite;
}
@-webkit-keyframes cycle1 {
 0% {opacity:0}
 5% {opacity:1}
 45% {opacity:1}
 50% {opacity:0}
}
img#animation2 {
    -webkit-animation:cycle2 10s infinite;
}
@-webkit-keyframes cycle2 {
 50% {opacity:0}
 55% {opacity:1}
 95% {opacity:1}
 100% {opacity:0}
}

Not bad. It does what we want, but I think it needs to be cleaned up. First, we’ve hard coded the duration time, so move that into a constant. Maybe we should move those to methods also. Yep, I’m picky

slideshow.rb

PICTURES = 2  
SLIDEDURATION = 5  

@offset = 0

def slideShowDuration
  (PICTURES * SLIDEDURATION).to_s
end

def setIDs i
  a = "img\#animation" + i.to_s + " {\n"
  a = a + " -webkit-animation:cycle" + i.to_s + " " + slideShowDuration + "s infinite;\n"
  a = a + "}"
  puts a
end

def animation i
  a = "@-webkit-keyframes cycle" + i.to_s + " {\n"
  a = a + " #{@offset}% {opacity:0}\n"
  a = a + "  " + (10 / PICTURES + @offset).to_s + "% {opacity:1}\n"
  a = a + "  " + (90 / PICTURES + @offset).to_s + "% {opacity:1}\n"
  a = a + "  " + (100 / PICTURES + @offset).to_s + "% {opacity:0}\n"
  a = a + "}"
  puts a
  # increase the offset
  @offset = 100 / PICTURES + @offset
end

for i in 1..PICTURES
  setIDs i
  animation i
end

When you run that you should see the same output as before. Change the constants and rerun the file. Notice the percentages and the duration change.

Tying It All Together

In the main.rb file, we display the pictures by looping through them.

<% @pictures.each do |picture| %>  
  <img src="<%= picture.sub!(/public\//, '') %>" />  
<% end %>

We need to add the id selector to them. In the slideshow.rb file we were using img#animation. Go ahead and code that up.

<% @pictures.each_with_index do |picture, i| %>  
  <img id="animation<%= i %>" src="<%= picture.sub!(/public\//, '') %>" />  
<% end %>

I used the .each_with_index to easily add numbers to the ids for the image tags.

Waste Nothing.

Remember the slideshow.rb file we wrote? Let’s tie that into this. We had a couple of constants in there and one of them needs to be replaced by a variable. I replaced PICTURES with @pictureslength. I also removed the loop at the bottom.

If we to leave it like that, we will have a problem. The @offset instance variable is never initialized. Since nil does not have a #+ method we will get an error. nil can't be coerced into Fixnum We should properly initialize @offset.

def initialize
  @offset = 0
end

Here’s what I have for slideshow.rb

SLIDEDURATION = 5

def initialize
  @offset = 0
end

def slideShowDuration
  (@pictureslength * SLIDEDURATION).to_s
end

def setIDs i
  a = "img\#animation" + i.to_s + " {\n"
  a = a + " -webkit-animation:cycle" + i.to_s + " " +     slideShowDuration + "s infinite;\n"
  a = a + "}"
  a
end

def animation i
  a = "@-webkit-keyframes cycle" + i.to_s + " {\n"
  a = a + " #{@offset}% {opacity:0}\n"
  a = a + "  " + (10 / @pictureslength + @offset).to_s + "% {opacity:1}\n"
  a = a + "  " + (90 / @pictureslength + @offset).to_s + "% {opacity:1}\n"
  a = a + "  " + (100 / @pictureslength + @offset).to_s + "% {opacity:0}\n"
  a = a + "}"

  # increase the offset
  @offset = 100 / @pictureslength + @offset

  a
end

Back in the main.rb file you need to set the @pictureslength variable to the length of pictures.

get '/' do
  @pictures = load_pictures
  @pictureslength = @pictures.length
  erb :index
end

Let’s look at the @@index template. We need to add our CSS to it. After the title tag add the following code:

<style>
  <% @pictures.each_with_index do |picture, i| %>
    <%= setIDs i %>
  <% end %>
  <% @pictures.each_with_index do |picture, i| %>
    <%= animation i %>
  <% end %>
</style>

Don’t forget that main.rb now requires the slideshow.rb file. Go ahead and add that at the top of the file.

Now, if you run the app, do the pictures fade in and out?

Make It Nicer with Some More CSS.

Since this is a slideshow, we want to see only one photo at a time. We can set the opacity to zero for all images and the transitions will make them appear. The images should also be at the top of the page and as wide as the browser window. Oh, and a nice dark background

html, body {
    background-color: #333;
    margin:0;
    padding:0;
}
img {
  position: absolute;
  width: 100%;
  opacity:0
}

That does all of it. Put that in the index page at the beginning of your style section.

main.rb

require 'sinatra'
require './slideshow'

def load_pictures
  Dir.glob("public/slideshow_pictures/*.{jpg,JPG}")
end

get '/' do
  @pictures = load_pictures
  @pictureslength = @pictures.length
  erb :index
end

get '/upload' do
  erb :upload
end

post "/upload" do 
  File.open('public/slideshow_pictures/' + params['file'][:filename], "w") do |f|
    f.write(params['file'][:tempfile].read)
  end
  "uploaded."
end

__END__
@@index
<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="user-scalable=yes, width=device-width" />
<title>Moving Pictures</title>
  <style>
    html, body {
        background-color: #333;
        margin:0;
        padding:0;
    }
    img {
      position: absolute;
      width: 100%;
      opacity:0
    }
    <% @pictures.each_with_index do |picture, i| %>
      <%= setIDs i %>
    <% end %>
    <% @pictures.each_with_index do |picture, i| %>
      <%= animation i %>
    <% end %>
  </style>
</head>
<body>
  <% @pictures.each_with_index do |picture, i| %>
    <img id="animation<%= i %>" src="<%= picture.sub!(/public\//, '') %>" />
  <% end %>
</body>
</html>

@@upload
<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="user-scalable=yes, width=device-width" />
<title>Upload Moving Pictures</title>
</head>
<body>
  <form action="/upload" enctype="multipart/form-data" method="post">
    <label for="file">File to upload:</label>
    <input id="file" type="file" name="file">
    <input id="submit" type="submit" name="submit" value="Upload picture!">
  </form>
</body>
</html>

When is the last time we ran the tests? Do you think they’ll pass?

$ ruby test/test.rb 
Run options: --seed 42735

# Running tests:

....

Finished tests in 0.073608s, 54.3419 tests/s, 81.5129 assertions/s.

4 tests, 6 assertions, 0 failures, 0 errors, 0 skips

Whew. Let fire up the app and view all of the different stages live

Now we have an app that can upload pictures and view them as a slide show.

Let’s go over what we did in this series.

  • Read files from a directory.
  • Constrain file types.
  • The default folder structure of a Sinatra app.
  • Create our own error messages.
  • How to write a bad test.
  • See if files exists.
  • Delete files.
  • Include other files.
  • Animation and keyframes in CSS.

I hope you enjoyed this series and learned something about Sinatra, Ruby, and/or CSS

John IvanoffJohn Ivanoff
View Author

John is a Dallas-based front-end/back-end web developer with 15+ years experience. His professional growth has come from big corporate day jobs and weekend freelance. His is enjoys working the Ruby the most these days and has even added pain to the process by developing Rails for Windows! He’s had many years of enjoyment with Cold Fusion and has strong background in web standards.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week