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
<% @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 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.