The bungalow was not the only Craftsman home built for those of modest means. The four-square was adapted and simplified to become a more modest house.

The modest four-square is smaller and the details are simplified. A gable-ended roof replaces the hipped one. Shed dormers (front home) or gable-ended dormers (rear home) replace the more expensive hipped dormer.

These homes appear in abundance where modest homes were built in the 1920s.

Modest Craftsman four-square homes, Warwick, NY, c. 1925.

Modest Craftsman four-square homes.

Edges to Rubies The Complete SketchUp Tutorial


Light-weight biplane.

Chapter 16—Professional Animation

We'll build a professional animation using the Animation interface and the View class. I'll also introduce classes that almost entirely bury the Transformation Matrix, letting you move, rotate and scale without a Transformation object or method. But first lets have some fun.

Airshow! a SketchUp Movie

Airshow! is a fun, three-minute movie. It's also a very serious bit of Ruby programming. It uses the techniques we'll be looking at in this chapter, including the Transformation-free classes, the Animation interface, the View class, an Animator and the Action interface. Put together they take SketchUp into the realm of professional animation.

I'd show it to you here, but there's no way to embed SketchUp models in HTML pages. (At least not yet.) The only movement we can get here is with a marquee.

Download and enjoy the movie.

Was that fun? Remember that it demonstrates all the techniques we'll cover in this chapter. Before you're done with this chapter you'll be able to move, rotate and scale any geometry you create. Begin thinking about what you want to animate. Your project will be to create your own movie (airplanes not allowed!).

Losing the Transformation Matrix

You know, we've been too serious for too long. How about a fairy tale?

Slaying the Transformation Monstrix

Once upon a time, in a land not unlike our own, in an early and difficult time, Ruby API developers were faced with a desparate challenge: the Transformation Monstrix! Breathing fire, eating small children and making plugins development a job for only the strongest and most fearless of developers, the Xform Monstrix (no one dared mention its real name) kept model objects firmly in their places, unable to rotate, scale, move and dance.

Then there came a developer from a foreign land, a land named Simple. He heard the tales of the Xform Monstrix and he said, "This is ridiculous. There should be no such terror here."

The foreign developer left. Persons who had hoped he might do something wept. But a few, those who drank at the SketchUcation Tavern on the outskirts of town, kept getting letters with questions from the foreigner. What was the Monstrix like? What did it eat? What fueled its fiery breath? How thick was its hide?

Of a sudden, the questions stopped coming. Those who thought about it, and few did as those who drank at the SketchUcation Tavern were most busy with their own affairs, thought the worst. "Poor fellow. Seemed a decent chap." "Yup. Shouldn't have been mucking around with that monstrix."

Then, on the coldest day of the winter, the developer from Simple returned. "I have slain the Xform Monstrix," he announced. And with that he withdrew two scrolls from the bag he carried. They were covered with strange markings unknown to the townspeople. One said, "Wait! The peculiar ones who drink at the SketchUcation Tavern know of such things. We shall go ask them what this means."

And they did. The peculiar ones' drinking, singing and writing their own strange scrolls all stopped while the two new scrolls were examined. "Yes!", one exclaimed. "These are made from the hide of the Xform Monstrix. It must be slain!"

And with that, the fear left the people and their models started to rotate, scale, move and dance and the Monstrix never again was seen and they all lived happily ever after.

Transformable ComponentInstances

That fairy tale had a traditional ending, believed only by small children. Since I doubt that any of you are still small children, I'll give you an adult replacement for the last paragraph.
And with that, the fear left the people and their models started to rotate, scale, move and dance and they did not see the Monstrix. One of the peculiar ones, older and wiser than the rest, muttered, "Yes, this slaying of monstrixes is a dangerous business, indeed." Those who heard this thought that he was praising the foreigner's cunning and courage, but this was not at all his meaning. The wise one was thinking about Appendix T.
In the last chapter you worked with the Transformation Matrix. Mostly you learned how to use the Transformation class's methods to avoid getting too chummy with that bit of mathematics. If you read Appendix T, you learned that a deeper understanding of the math didn't really help the programming.

My thought was that it was much too difficult. There was really no reason to have it visible at all. Why shouldn't ComponentInstances know how to move, rotate and scale themselves without troubling poor developers about the underware beneath these transformations?

Note to Google: A class between DrawingElement and classes that have a transformation matrix (Group and ComponentInstance today) might be nice: TransformableDrawingElement could extend DrawingElement and be extended by Group and ComponentInstance. The four classes that Group and ComponentInstance have in common (move!() transform!() transformation() and transformation=() could become methods of the TransformableDrawingElement class. My move() rotate() and scale() methods could also be methods of the TransformableDrawingElement class. And my code could be discarded! Really, writing matrix multiplication in a scripting language is the act of a desparate developer backed into a coding corner.

Until Google comes to the rescue, I've created a temporary fix—two classes with identical APIs. One is the TransformableCI class and the other is the MovableCI class. The difference between the two is that the first uses the ComponentInstance's transform!() method. This method causes a ComponentInstance to transform (move, rotate or scale); it places undo data on the undo stack and it causes the model to redraw.

A MovableCI uses the ComponentInstance's move!() method. This method causes a ComponentInstance to transform, but it doesn't add to the undo stack and it doesn't cause the model to redraw. It is a transform!() optimized for animations. (At least that's what the design looks like. The fact that move!() doesn't work has limited its acceptance. As written it does a transformation = when it should do a transformation *=.)

The outer APIs of the TransformableCI and MovableCI classes are identical.

The Transformable and Movable API

You create the TransformableCI or MovableCI this way:

    movable = TransformableCI.new( ComponentInstance )
    # or 
    movable = MovableCI.new( ComponentInstance )
 

You might want to start with the TransformableCI class if you are going to experiment in your console. If you ask a MovableCI to move(), rotate() or scale() it will just sit there as if you had said nothing at all. It takes a Sketchup.active_model.active_view.invalidate to persuade SketchUp to redraw. This is no problem in animation code, but it gets old fast in a console. You can develop an animation at, say, two frames per second using the TransformableCI. As the API is identical, you just change Transformable.new to Movable.new and you are set for 24 frames per second.

There are three transformations: translation (here known as "move"), rotation and scaling. This is the full outer API:

    movable.move( Point3d )
    movable.move( Vector3d )
    movable.move( [r,g,b] )
    movable.move( r, g, b )

A move( Point3d ) moves to the specified point. The others move by a vector from the current location.

    movable.rotate( Point3d, plane_or_axis, degrees )

    movable.scale( scale_factor )
    movable.scale( point, scale_factor )
    movable.scale( rscale_factor, gscale_factor, bscale_factor )
    movable.scale( point, rscale_factor, gscale_factor, bscale_factor )

"point"—a Point3d or an [r,g,b] array. If the "point" is not specified, it defaults to the origin, [0,0,0].

At present (April, 2010) SketchUp 7.1 has bugs that are triggered by a value other than one in the global scale (Wt). The first two forms of movable.scale() are very convenient if you are working in a console but should not be used in code.

Return values for all forms of move(), rotate() and scale() are not specified and may change over time.

Scaling about an arbitrary point is new. The Scale Tool scales about either the opposite handle or about the center. What does it mean to scale about an arbitrary point? [20,0,0] moves to [40,0,0] if you scale by 2 about the origin. More generally, if you scale by 2 every vertex is moved twice as far from the scaling point. Fully generally:

    # X is one of r, g or b
    new_locX = old_locX + scale_factor*(old_locX-scaling_pointX)

The TransformableCI Class

Your browser's default behavior for a Ruby file may be to display the source code. The "download" then becomes select all; copy to clipboard; open a new file in your code editor; paste from clipboard and File/Save As... .

Download the foreigner's first scroll to your convenience directory as transformable_ci.rb.

Download? No typing?

Right. No typing. The whole point of the TransformableCI and the MovableCI is to let you forget all the details of mucking about with Transformation methods and objects. That's a thing of the past. (At least for now.) After these, however, this chapter will show almost no code, letting you write everything else you need. You'll do lots of coding.

In SketchUp, create and select a ComponentInstance. In the Ruby Console:
load '/your/dir/transformable_ci.rb'

Get a reference to your instance:
foo = Sketchup.active_model.selection[0]

Then create a TransformableCI reference:
foo = TransformableCI.new( foo )

Then start experimenting. foo.move 10, 0, 0 would be a logical start. foo.rotate [0,0,0], 'rb', 45 will change things. foo.scale 2 will grow things. Have fun!

If it gets to be too much fun, take a look at the source code. This bit is from the move() method:

# your code:
    movable.move( [1,2,3] )
    
# the underware:
        elsif arg.is_a?( Array )
            vec = Geom::Vector3d.new( args[0] )
            xform = Geom::Transformation.new( vec )
            @inst.transform!( xform )

In the bad old days, you had to write the underware to do a simple move(). Makes you realize that the pioneers at the SketchUcation Tavern were a tough breed indeed!

The MovableCI Class

Download the foreigner's second scroll into your convenience directory as movable_ci.rb. If you want to experiment with it in your Ruby Console, add a little draw() function that does Sketchup.active_model.active_view.invalidate() into your SketchTalk.

Remember that this is designed so you can use it in animations. The basic idea is that you might bank your plane (rotate around the red axis), turn (rotate around the blue axis), move it and then redraw. You'll need to draw to see any changes in your Ruby Console.

If you compare this code to the TransformableCI code you'll see that it is much more complex. The primary cause is that the Ruby API's ComponentInstance.move!() does not work (as of February, 2010, SketchUp 7.1). It should apply new transformations to the existing ones. Unfortunately, it simply replaces the existing transformation. To get it right required more work.

The MovableCI_Matrix class (it follows the MovableCI class in the same file) implements two operators: [] and *. In general, I don't do this. C++ allowed you to override operators. It was messy and error-prone. (One of the features that Java wisely dropped.) In this case, I thought it was worth it to have the calling code able to do stuff like m1[3,3] (gets the global scale factor from row 4, column 4) and m1 * m2 (multiplies two matrices, see Appendix MM). Take a look at the code if you want to see how it's done. Don't overdo it once you know how.

The move() and scale() each call other methods to perform their work. You can call these other methods directly if your animation demands that every wasted clock be eliminated. Bear in mind that if you call these methods you will not be able to change your MovableCI back to a TransformableCI as these don't exist in the TransformableCI.

    move_to( r, g, b ) # move( Point3d )
    move_tw( r, g, b ) # move( Vector3d ) - move_ToWard()
    
    scale_g( global_scale_factor )
    scale_pg( point, global_scale_factor )
    scale_rgb( rscale_factor, gscale_factor, bscale_factor )
    scale_prgb( point, rscale_factor, gscale_factor, bscale_factor )

You now have tools to move, rotate and scale your components, untroubled by Transformation objects and methods. Now it's time to put them to use.

Professional Animation—Implmenting an Interface

Sun Microsystems (on its way to becoming part of Oracle as I write this) introduced Java in 1995. Designed for Internet-based applets, Java strove to be light in weight. It discarded C++ complexities, an effort that included replacing multiple inheritance (classes that extend multiple classes) with interfaces.
Topic: Interfaces

An interface is a set of method names with defined parameters and return values. The interface does not implement the methods. That is up to a class that wants to be callable by something that is expecting to find these methods.

The best known example is Java's Runnable interface. To be Runnable, a class has to implement just one method, run(). It has no arguments and no return value.

Runnable is very important as a Java Thread can be created if its constructor is passed a Runnable. When the system gives control to the Thread, the Thread calls the Runnable's run() method. The Thread has no clue what run() does, it just knows that the Runnable has a run() method that it can call.

In the Ruby API, Animation is an interface. It specifies four methods:

Unlike Java which requires that a class declare any interfaces it implements, and the compiler checks the code to be sure that the interfaces' methods are actually present, Ruby has no interface-checking mechanism. If you want to implement nextFrame() and nothing else, that's fine.

You pass your Animation to the active View. An Animation is anything that implements a nextFrame() method. It could be a car, truck, ferris wheel, carousel, sailboat, ... Anything that doesn't want to look the same all the time. The View calls your Animation's nextFrame() method whenever it can take a break from doing the other things a View does. It tries to call often (many times each second, if possible). If the Animation is done, the View calls your Animation's stop() method, if there is one. The View has no clue what nextFrame() does. Our nextFrame()s will handle moving, rotating and scaling our MovableCI objects.

Let's take a very small example. This is the point at which you resume typing. While you're typing, substitute your folders for mine.

# /r/anim1.rb - sample Animation

require 'sketchup'
load '/r2/transformable_ci.rb'

class Anim
    
    def nextFrame( view )
        $mvbl.move( Geom::Vector3d.new(1, 0, 0) )
        Sketchup.active_model().active_view().invalidate()
        $i += 1
        return $i < 10
    end
    
end

$i = 0
$mvbl = TransformableCI.new( Sketchup.active_model().selection()[0] )
Sketchup.active_model().active_view().animation = Anim.new()

To use this code, model or import some geometry. I've just got a box. Select it. Load anim1.rb. Zooop! Your selection slides 10 units in the positive red direction. Load it again, and Zooop! Don't overdo it. Watching a box slide gets old quickly. Animation, however, does not get old.

Our Anim class has a minimal nextFrame() method. It moves a TransformableCI one unit at a time in the positive red direction. It increments a counter and returns true until the counter reaches ten. Returning true tells the View that it wants to keep going. Returning false tells the view to terminate it.

On your own, add a stop() method. My first one said, puts "Putting out the trash". My second one hopped the TransformableCI back to where it started.

Your next job: Make an Anim2 that is based on the MovableCI class. It's a lot faster, so maybe a hundred steps would be better than ten. For maximum speed, use the move_tw(). Don't forget to invalidate the view in the nextFrame() or you'll have nothing to look at.

Your last job: time it. Before you pass the Animation to the View, $start = Time.now(). In your stop() method, puts Time.now() - $start. By the way, Time is a large and interesting class. You probably don't need my help to understand the Time.now() method. A little time spent Googling Ruby Time will be rewarding. (You may want to wait until you've organized a good Ruby reference tab in your reference browser—Chapter 17.)

My Windows machine is quite old. I got about 1.6 seconds for those hundred steps. That's running at roughly 60 frames per second. How to control it (sort of)? The View calls nextFrame( view ), passing a handy reference to itself. The view.show_frame() method handles invalidating (and therefore redrawing) the view. view.show_frame( 1.0/24.0 ) adds a delay. I'm up to 4.7 seconds for the 100 steps, about 21 frames per second. I need to shorten the delay to get an effective 24 frames per second. Animation is art, not science. You'll see that our Animator class checks and adjusts timing every 100 frames.

So far, so good. Now we're on to the camera. You do not move and rotate the camera. It has its own API and not a small one. Fortunately, the API you need is a very small one. I'll introduce you to it and then mention the rest of the API, explaining why you don't need it unless you want to remake Hitchcock's Vertigo in SketchUp.

The Camera API

There is a Camera that determines what SketchUp draws. When you orbit a model you are moving the camera. When you zoom you are changing the camera's field of view. (The SketchUp camera is more precise than our legendary spy satellite cameras. It can read the text in this tutorial from outer space. There is no dust in the SketchUp atmosphere.)

To get a reference to the Camera: cam = Sketchup.active_model().active_view().camera(). To zoom in, narrow the field of view: cam.fov /= 2. To control position and direction, use the set() method: cam.set( new_point_to_look_from, new_point_to_look_at, [0,0,1] ). The camera is looking from cam.eye() and looking at cam.target(). These are Geom::Point3d objects, but [r,g,b] arrays work, too. More concisely, basic position/aim code is cam.set( new_eye, new_target, [0,0,1] ).

The Google API states that Camera.set() returns a new Camera object. It does not. It returns the reference cam in cam.set(). Ignore it.

That's really all you need to know. I was worried about the speed of Camera.set(). I timed one. Time.now - start reported "0.0 seconds". I tried setting the camera a thousand times (panning to follow a target). That took 0.06 seconds. In other words, setting the camera takes about 0.1% of the time available for a single frame of a 24 frames-per-second animation.

Is it really that simple?

Here is a quick rundown on the rest of the API.

aspect_ratio(), aspect_ratio=()You can read and set the aspect ratio. That lets you lose pixels, like this:

Aspect ratio too wide for window.  

The default aspect ratio is the View's aspect ratio which is determined by the window's shape (or the screen's shape, if maximized). Any other aspect ratio causes lost pixels. Assigning 0 to the aspect_ratio returns to the default, window-based ratio. (You may want to remember this for special effects, such as simulating wide-screen views on a TV monitor.)

description(), description=()You can assign a description to your Camera. (Possibly useful if you have multiple cameras?)

direction() is a normalized vector pointing from the camera's eye toward the camera's target. If your camera follows a moving object (in Airshow! the camera follows Biplane's bounds().center()) you'll have little use for this vector.

eye(), target()You can ask for the eye position and the target position. If you have just set them, this is not terribly helpful.

focal_length(), image_width(), focal_length=(), image_width=()You can inquire about and set the focal_length and image_width. Both these provide an alternate to just setting field-of-view. I'm not fond of having multiple ways to do a single thing. (Monty, my pet python, is still looking to give Tim Toady a big hug. You do not want Monty's big hug.)

perspective?(), perspective=(), height=()Your camera is a perspective camera by default. You can change to an orthographic camera and set its height. This might be useful if you were modeling drafting-type work.

set( eye, target, up )The third parameter to the set() method doesn't need to be set to [0,0,1]. If you ever want to model a very advanced mental illness, spinning your view with this one might be just the thing. (Warning: spinning your view with this one might give you the very advanced mental illness. Just try [0,0,-1] to feel like a character in Hitchcock's Vertigo.)

up(), xaxis(), yaxis(), zaxis()Last, you can ask which way is up. If you've used [0,0,1] in your set() calls, the "up" vector will be the direction of the top of the camera, as held by a sober photographer. This will also be the "yaxis". The "zaxis" is a normalized vector from the camera's eye to its target (the same as direction()). The "xaxis" is perpendicular to the plane of the camera direction and the "up" vector. You could find this by taking the cross product of the yaxis and zaxis vectors. (If you don't know how that's done, the "*" operator works: yaxis() * zaxis().) Why you would want a vector pointing out the camera's left or right side is a mystery to me.

Ready to experiment? Create or import some geometry. Select it. Group it. Pause for some more SketchUp API. Things like groups, component instances, construction guides and edges all inherit from the DrawingElement class. Every DrawingElement has a bounds() method that returns a reference to a BoundingBox object. Among many others, a BoundingBox has a center() method that returns a Point3d.

Biplane, 35 degree fov.  

Now let's use that new API knowledge. In the Ruby Console with SketchTalk loaded: target = sel.bounds.center. Grab a camera: cam = Sketchup.active_model.active_view.camera. Now take a look from a long way away and five feet up: cam = cam.set [-1200,-1200,60], target, [0,0,1]. Here's Biplane, 35 degree field-of-view.

Biplane, 5 degree fov.  

Here's Biplane again, same eye location, after cam.fov=5.

Biplane, closer, 35 degree fov.  

Once more, from close up with a 35-degree field-of-view.

If you look closely, you see that the greater distance increases the darkness of the shadows, but otherwise has no impact. Conclusion? This is like photography. Being close to your subject is always better than zooming in from farther. But it's like photography when you have a powerful lens, a sturdy tripod and a windless, clear, sunny day.

Run your own experiments with camera positions, targets and fields of view. We'll wait.

The MovableCamera Class

My MovableCamera sits on top of the Camera API and picks up from the Movable/TransformableCI API. Give this code one careful read (it's all you've got for documentation) and then copy it into the same folder that holds your Movable/TransformableCI class files.
# /r2/movable_camera.rb

require 'sketchup'

class MovableCamera 

    attr_accessor :camera

    def initialize( cam )
        @camera = cam
    end # of initialize()
    
    def move_to( r, g, b )
        @camera.set( [r,g,b], @camera.target, @camera.up )
    end

    def move_tw( r, g, b )
        eye = @camera.eye()
        @camera.set( [eye[0]+r, eye[1]+g, eye[2]+b], 
            @camera.target, @camera.up )
    end # of move_tw()
    
    def target( r, g, b )
        @camera.set( @camera.eye, [r,g,b], [0,0,1] )
    end # of target()
    
    def move_tw_and_target( r1, g1, b1, r2, g2, b2 )
        @camera.set( camera.eye()+[r1,g1,b1], [r2,g2,b2], [0,0,1] )
    end # of move_tw_and_target()
    
    def pan( r2, g2, b2 )
        @camera.set( 
            @camera.eye, @camera.target + [r2, g2, b2], [0,0,1] )
    end
    
    def zoom_to( fov )
        @camera.fov = fov
    end
    
    def zoom_tw( zoom )
        @camera.fov += zoom
    end
    
end # of class MovableCamera
If your folder is not /r2/, correct the comment.

Design of the Animation System

Now it's time to design an animation system.

How do you approach a design? The first step is to turn your code editor off. Fire up a program like FreeMind and write out your goals. (If you don't have your own favorite, "help me think this problem through" software, I highly recommend Freemind. But before you download it, see Appendix F - Freemind.)

These were the most important goals I had in mind.

At this point I decided to write Airshow!, my first SketchUp movie. I started with a script, written in a spreadsheet:

Spreadsheet outlining Airshow! movie.

I wanted the director (that was me) to work at this script level, a few seconds at a time. So, invent an interface: Actionable. An object is Actionable if it has an act( frame_number ) method and a duration() method. The former actually does something. The biplane's Taxi object has an act() method that accelerates down the runway. The Actionable's duration() method is called by Animator, the Animation controller (boss of all the Actionables) so the proper number of frames will be passed to, for example, the Taxi code.

So the overall design is this:

One important consideration in writing the Actionables: they are all initialized before the movie starts. As much as possible is precalculated in the Actionable's constructor rather than in the act( frame_number ) method. Here's an example. The Taxi object accelerates from zero to a predetermined speed over its duration. Here's a bit of the mainline that initializes the Taxi object:
actions = [ 
    BiplaneBackup.new( 3 ),

        # Sang moves omitted
        
    Taxi.new( 4 ),
    Takeoff.new( 2 ),
    Climb.new( 4 ), 
    LevelOff.new( 2, 20 ),
    ...
]
(Those arguments are durations, in seconds.) Now here's a simplified bit of the Taxi class—its runway is also the red axis:
class Taxi # start of flight: Taxi, Takeoff

    attr_reader :duration
    
    def initialize( duration )
        @duration = duration
        @frames = duration * $frames_per_second
        end_speed = 44 # inches/frame = 60 mph = 88fps
        @delta = end_speed / @frames
        @speed = 0
    end

    def act( frame_number )
        $airshow_biplane.move_tw( @speed, 0, 0 )
        @speed += @delta
    end # of act()

end # of class Taxi

This act() method ignores the frame_number. Others use it. For example, the sweeping turn begins by banking in the first few frames, then rotates through the turn, then unbanks. (No, it does not begin slight rotation when it begins banking. Aeronautically correct? No. Does it look like a turn? Yes!)

Don't forget that attr_reader :duration. The Animator needs to read each object's duration.

It turns out that putting the duration into the mainline was very important. How long should the taxi take? Originally it was 2 seconds, as I thought it would be uninteresting. Turned out that the camera catches the viewing stand's railings, which made it very interesting. Doubled the time to 4 seconds.

The Animator Class

If you are into patterns, the Animator is a Singleton. Exactly one Animator is instantiated from the Animator class. (That's not enforced by code within the Animator class, it's just the way I used it.)

Here's the class, in its entirety, interspersed with my notes.

# animator.rb - real animation, final design
# from Edges to Rubies - the Complete SketchUp Tutorial
# documented in Chapter 16
# Copyright 2010, Martin Rinehart

class Animator
    def initialize( action_stack )
        @current_frame = 0
        @next_change_frame = 0
        @start_frame = 0
        @start_time = Time.now()
        @action_stack = action_stack
        @action_stack_ptr = 0
    end # of initialize
    
	def nextFrame( view )

The constructor is perfectly straightforward. The nextFrame() method is the interesting one. We'll get to it. The adjust_timing() method is called once every hundred frames to speed up or slow down to achieve the target frame rate.

		
	end # of nextFrame()
    
    def adjust_timing()
        return if @current_frame == 0
        time_sb = @current_frame / $frames_per_second
        time_is = Time.now - @start_time
        $frame_delay_in_seconds *= ( time_sb / time_is )
    end
    
    def stop()
        puts Time.now() - $airshow_start
    end

end # of class Animator

# end of animator.rb

Now, on to the Animation interface, nextFrame().

    def nextFrame( view )
    
        if @current_frame == @next_change_frame
            return if @action_stack_ptr == @action_stack.length()
			@action = @action_stack[ @action_stack_ptr ]
            @action_stack_ptr += 1
            @next_change_frame = @current_frame + 
                ( @action.duration() * $frames_per_second )
            @start_frame = @current_frame
        end

The first part contains the code that is called to move from one Actionable to the next. (@current_frame and @next_change_frame are both initialized to zero, so this code runs at frame zero.)

The first thing is a check to see if we are done. If not done, the next Actionable is placed in @action and the stack pointer is incremented. The @next_change_frame is computed from the current frame and the number of seconds times the frame rate. Finally, the @start_frame, the frame at which this Actionable starts, is set.

The remainder of the method is called for every frame.

Topic: Modulo

The modulo operator gives you a remainder after division. In Ruby the % sign is pressed into service. 1 % 2 is 1. 2 % 2 is zero. 3 % 2 is 1, and so on.

In the following code, if (@current_frame % 100) == 0 is used to call a method at frame 100, again at frame 200 and so on.

        
        @action.act( @current_frame - @start_frame )
			# note: frame number is relative to start of action
        view.show_frame( $frame_delay_in_seconds )
        adjust_timing() if (@current_frame % 100) == 0
        @current_frame += 1
        
    end # of nextFrame()

First, the Actionable's act() method is called with the frame number relative to the start of the Actionable. The view's show_frame() method is called with a delay. This is where the view will actually be redrawn. Then the adjust_timing() is called for frames divisible by 100. Last, the count is incremented.

There is nothing magic going on here. I've broken this code up to be mean to the "I'll just copy, not type" crowd. You can write your own. Remember, the goal here is to not waste clocks. The bit that runs every frame has got to be light. If yours is lighter than mine, I want to know about it! Click me on my website for an email address.

If you do write your own, fix this problem: this Animator is not really freindly toward multiple moving objects. I've done others that are better, but they've all come out as very complex and quite a bit heavier than this one. That's an unsolved problem that really needs to be solved.

Sample Scene Class

The individual scene classes may or may not be singletons. Biplane taxis and takes off once, for instance, but the sweeping turn at both ends of the circuit happens three times. Let's pick apart our most complex scene. It's the one where Sang mounts the camera on the tripod.

class GoodByeSang

    attr_reader :duration
    
    def initialize( duration, wait )
        @duration = duration
        @frames = duration * $frames_per_second
        @mounting_frames = ( duration - wait ) * $frames_per_second

This constructor has a second paramter, wait. It is the number of seconds (it's 2) to wait at the end of the total duration (4). The @mounting_frames is the frames for mounting the camera and moving Sang behind it.

        
        @last_frame = @frames - 1

        @cam = nil
        @cam_per_frame_r = 27.5 / @mounting_frames
        @cam_per_frame_g = 3.0 / @mounting_frames
        @cam_per_frame_b = 28.4 / @mounting_frames
        @cam_rot_per_frame = 90.0 / @mounting_frames

I ran the movie up to the start of this scene. Then I grabbed a tape measure and carefully measured the distance we had to move the camera in each of the three dimensions. Dividing these distances (and the 90 degree rotation) by the number of mounting frames gives the amount to move and rotate, each frame.

        @sang = nil
        @sang_per_frame_r = 24.0 / @mounting_frames
        @sang_per_frame_g = -24.0 / @mounting_frames
        @sang_per_frame_b = 0.0

Similarly, a tape was applied to Sang. No rotation was required because Sang is a 2D, always-face-camera object.

        
        @eye = [0, -520, 285]
        @target = [0,7000,285]
        @fov_normal = 35

    end # of initialize()

The camera's eye is on top of the tripod. It points at a house in the distance (more Tape measuring). 35 degrees is a standard for field of view.

So far so good? There are complications. First, 2D Sang and a 3D camera were a ComponentInstance. (It was the instance climbing the stairs. Did you notice that the camera was in Sang's right hand when he moved to the right, but in his left when he moved left?) We'll need to explode this instance to move Sang and camera separately.

   
    def act( frame_number )
        if frame_number == 0
            puts 'moving rotating objects'
            ents =$airshow_sang.inst().definition().entities()
            @sang = MovableCI.new( ents[0] )
            @cam = MovableCI.new( ents[1] )
        end # separate Sang, camera

By picking Sang and the camera apart in the console, I learned that Sang was first and the camera second. Here they are both used to create separate MovableCI objects in the first frame.

        if frame_number <= @mounting_frames
            center = @cam.inst().bounds().center()
            @cam.rotate( center, 'rg', @cam_rot_per_frame )
            
            @cam.move_tw( 
                @cam_per_frame_r, @cam_per_frame_g, @cam_per_frame_b )
            @sang.move_tw( 
                @sang_per_frame_r, @sang_per_frame_g, @sang_per_frame_b )
        end
For the mounting frames, three things are happening. First, the camera (the 3D model camera) is rotating. Second, it is also moving. Third, Sang is moving into position behind the camera.

        # here doing nothing for duration of wait
        
        if frame_number == @last_frame
            $airshow_camera.camera = $airshow_camera.camera().set( 
                @eye, @target, [0,0,1] )
            $airshow_camera.camera().fov = @fov_normal
            
            @sang.inst().visible = false
        end
    end # of act()
    
end # of class GoodByeSang

Last, we wait until the last frame. At the last frame we abruptly change from the camera photographing Sang and the model camera, to the view that is seen looking through the camera on the tripod. We also turn Sang invisible. (A later addition. When Biplane buzzes the camera, he also buzzes Sang. As the camera turns to follow Biplane, Sangs head suddenly and alarmingly fills the screen. Poor Sang. I was going to teach him to duck, but never got around to it.)

Back at the SketchUcation Tavern, the wise old peculiar is talking privately to the developer from Simple. "I'm thinking about Appendix T.", he began.

"Yes?"

"Well, you just said @cam.rotate and then @cam.move."

"Yes.", agreed the somewhat abashed developer from Simple.

"Isn't the point to multiply the monstrixes first, and then fixup those vertices?"

"You're right. This monstrix-free code is simple, but it wastes a lot of clocks."

"And one other thing," the wise old peculiar one continued. "Those scrolls you brought back. That was monstrix hide, wasn't it? How'd you get it?"

The developer from Simple laughed. "Nicked a bit of monstrix tail. They don't mind. It grows right back. They're fictional, you know."

Your Mainline Program

Yes, that is your mainline, not mine. Got your project designed? Need ideas?

My mainline loads airshow.skp, imports BiPlane and Sang, and does other housekeeping. If you look at the code, you'll see that the title screen and the closing credits are WebDialogs. You'll need to get to Chapter 19 before that will make any sense. Skip them for now.

Start with a trivial mainline, steal my Animator and code one or two scene classes. Make something happen! Release a balloon and watch it rise. Then release a balloon and follow it with the camera as it rises. Once you get the hang of it, your problem will be that you have way too many good ideas. Good luck!

In Chapter 17, we'll organize a very slick, powerful system to put reference material for all of the SketchUp Ruby API, plus the Ruby library classes, plus HTML tags and more at your fingertips. Everything you need to know will be just a click or two away.

If you feel you need a look at my code, the Tutorial Companion Package for this chapter is the movie that you downloaded at the beginning of the chapter.


Transformation matrix WebDialog. View of apartment contents. Documentation frameset.