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.
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.
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.
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!).
You know, we've been too serious for too long. How about a fairy tale?
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.
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?
move!() transform!() transformation()and
transformation=()could become methods of the TransformableDrawingElement class. My
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
The outer APIs of the TransformableCI and MovableCI classes are identical.
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
scale() it will just sit there as if you had said nothing at all. It takes a
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].
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)
Download the foreigner's first scroll to your convenience directory as
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:
Get a reference to your instance:
foo = Sketchup.active_model.selection
Then create a TransformableCI reference:
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
# your code: movable.move( [1,2,3] ) # the underware: elsif arg.is_a?( Array ) vec = Geom::Vector3d.new( args ) 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!
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:
*. In general, I don't do this.
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.
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.
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:
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() ) 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
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.
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
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] ).
Camera.set()returns a new Camera object. It does not. It returns the reference
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=()You can read and set the aspect ratio. That lets you lose pixels, like this:
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=()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.
target()You can ask for the eye position and the target position. If you have just set them, this is not terribly helpful.
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.)
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.)
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.
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.
Here's Biplane again, same eye location, after
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.
# /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+r, eye+g, eye+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
/r2/, correct the comment.
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.
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:
next_frame(), the Animation interface. It takes the stack of Actionables from the mainline and grabs each Actionable in turn. It asks the Actionable how long it wants, and then calls the Actionable's
act( frame_number )for the appropriate number of frames, before moving on to the next Actionable.
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 ), ... ]
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
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.
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,
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. (
@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.
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
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.
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 ) @cam = MovableCI.new( ents ) 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
# 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.)
"Well, you just said
@cam.rotate and then
"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."
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.