Animation with Slider Controls in a GUIDE GUI

About the Example

This example shows how to create a GUI with 3-D axes in which the Earth spins on its axis. It accepts no inputs, but it reads a matrix of topographic elevations for the whole Earth. The GUI provides controls to:

  • Start and stop the rotation.

  • Change lighting direction.

  • Display a latitude-longitude grid (or graticule).

  • Save the animation as a movie in a MAT-file.

  • Exit the application.

To get and view the example code:

  1. Copy the example FIG-file and code file to your current (writeable) folder and open the FIG-file in GUIDE with the following commands:

    copyfile(fullfile(docroot, 'techdoc','creating_guis','examples',...
    'globegui*.*')), fileattrib('globegui*.*', '+w')
    guide globegui.fig
    
  2. From GUIDE Layout Editor, click the Editor button .

    The globegui.m code displays in the MATLAB® Editor.

Design the 3-D Globe GUI

In the GUIDE Layout Editor, the GUI looks like this.

The GUI includes three uipanels that you can barely see in this figure because they are entirely black. Using uipanels helps the graphic functions work more efficiently.

The axes CreateFcn (axes1_CreateFcn) initializes the graphic objects. It executes once, no matter how many times the GUI is opened.

The Spin button's callback (spinstopbutton_Callback), which contains a while loop for rotating the spherical surface, conducts the animation.

The two sliders allow you to change light direction during animation and function independently, but they query one another's value because both parameters are needed to specify a view.

The Show grid check box toggles the Visible property of the graticule surface object. The axes1_CreateFcn initializes the graticule and then hides it until you select this option.

The Spin button's callback reads the Make movie check box value to accumulate movie frames and saves the movie to a MAT-file when rotation is stopped or after one full revolution, whichever comes first. (You must select Make movie before spinning the globe.)

The Property Inspector was used to customize the uicontrols and text by:

  • Setting the figure Color to black, as well as the BackgroundColor, ForegroundColor, and ShadowColor of the three uipanels. (They are used as containers only, so they do not need to be visible.)

  • Coloring all static text yellow and uicontrol backgrounds either black or yellow-gray.

  • Giving all uicontrols mnemonic names in their Tag string.

  • Setting the FontSize for uicontrols to 9 points.

  • Specifying nondefault Min and Max values for the sliders.

  • Adding tooltip strings for some controls.

The following sections describe the interactive techniques used in the GUI.

Alternate the Label of a Push Button

The top right button, initially labeled Spin, changes to Stop when clicked, and back to Spin clicked a second time. It does this by comparing its String property to a pair of strings stored in the handles structure as a cell array. Insert this data into the handles structure in spinstopbutton_CreateFcn, as follows:

function spinstopbutton_CreateFcn(hObject, eventdata, handles)
handles.Strings = {'Spin';'Stop'};
guidata(hObject, handles); 

The call to guidata saves the updated handles structure for the figure containing hObject, which is the spinstopbutton push button object. GUIDE named this object pushbutton1. It was renamed by changing its Tag property in the Property Inspector. As a result, GUIDE changed all references to the component in the GUI' code file when the GUI was saved. For more information on setting tags, see Identify the Axes in the previous example.

The handles.Strings data is used in the spinstopbutton_Callback function, which includes the following code for changing the label of the button:

str = get(hObject,'String');
state = find(strcmp(str,handles.Strings)); 
set(hObject,'String',handles.Strings{3-state}); 

The find function returns the index of the string that matches the button's current label. The call to set switches the label to the alternative string. If state is 1, 3-state sets it to 2. If state is 2, it sets it to 1.

Interrupt the Spin Callback

If you click the Spin/Stop button when its label is Stop, its callback is looping through code that updates the display by advancing the rotation of the surface objects. The spinstopbutton_Callback contains code that listens to such events, but it does not use the events structure to accomplish this. Instead, it uses this piece of code to exit the display loop:

if find(strcmp(get(hObject,'String'),handles.Strings)) == 1
    handles.azimuth = az;
    guidata(hObject,handles);
    break
end

Entering this block of code while spinning the view exits the while loop to stop the animation. First, however, it saves the current azimuth of rotation for initializing the next spin. (The handles structure can store any variable, not just handles.) If you click the (now) Spin button, the animation resumes at the place where it halted, using the cached azimuth value.

When you click Quit, the GUI destroys the figure, exiting immediately. To avoid errors due to quitting while the animation is running, the while loop must know whether the axes object still exists:

while ishandle(handles.axes1)
    % plotting code
    ...
end

You can write the spinstopbutton_Callback function without a while loop, which avoids you having to test that the figure still exists. You can, for example, create a timer object that handles updating the graphics. This example does not explore the technique, but you can find information about programming timers in Use a MATLAB Timer Object.

Make a Movie of the Animation

Selecting the Make movie check box before clicking Spin causes the application to record each frame displayed in the while loop of the spinstopbutton_Callback routine. When you select this check box, the animation runs more slowly because the following block of code executes:

filming = handles.movie;
    ...
if ishandle(handles.axes1) && filming > 0 && filming < 361
    globeframes(filming) = getframe; % Capture axes in movie
    filming = filming + 1;
end

Because it is the value of a check box, handles.movie is either 0 or 1. When it is 1, a copy (filming) of it keeps a count of the number of frames saved in the globeframes matrix (which contains the axes CData and colormap for each frame). You cannot toggle saving the movie on or off while the globe is spinning, because the while loop code does not monitor the state of the Make movie check box.

The ishandle test prevents the getframe from generating an error if the axes is destroyed before the while loop finishes.

When the while loop terminates, the callback prints the results of capturing movie frames to the Command Window and writes the movie to a MAT-file:

if (filming)
    filename = sprintf('globe%i.mat',filming-1);
    disp(['Writing movie to file ' filename]);
    save (filename, 'globeframes')
end   

    Note:   Before creating a movie file with the GUI, make sure that you have write permission for the current folder.

The file name of the movie ends with the number of frames it contains. Supposing the movie's file name is globe360.mat, you play it with:

load globe360
axis equal off
movie(globeframes)

The playback looks like this.

Graphics Techniques Used in the 3-D Globe GUI

To learn more about how this GUI uses Handle Graphics® to create and view 3-D objects, read the following sections:

Create the Graphic Objects

The axes1_CreateFcn function initializes the axes, the two objects displayed in it, and two hgtransform objects that affect the rotation of the globe:

  • The globe is a surfaceplot generated by surface.

  • The geographic graticule (lines of latitude and longitude), also a surfaceplot object, generated by a call to mesh.

Data for these two objects are rectangular x-y-z grids generated by the sphere function. The globe's grid is 50-by-50 and the graticule grid is 8-by-15. (Every other row of the 15-by-15 grid returned by sphere is removed to equalize its North-South and East-West spans when viewed on the globe.)

The axes x-, y-, and z-limits are set to [-1.02 1.02]. Because the graphic objects are unit spheres, this leaves a little space around them while constraining all three axes to remain the same relative and absolute size. The graticule grid is also enlarged by 2%, which is barely enough to prevent the opaque texture-mapped surface of the globe from obscuring the graticule. If you watch carefully, you can sometimes see missing pieces of graticule edges as the globe spins.

Texture and Color the Globe

Code in the axes1_CreateFcn sets the CData for the globe to the 180-by-360 (one degree) topo terrain grid by setting its FaceColor property to 'texturemap'. You can use any image or grid to texture a surface. Specify surface properties as a struct containing one element per property that you must set, as follows:

props.FaceColor= 'texture';
props.EdgeColor = 'none';
props.FaceLighting = 'gouraud';
props.Cdata = topo;
props.Parent = hgrotate;
hsurf = surface(x,y,z,props);
colormap(cmap)

    Tip   You can create MATLAB structs that contain values for sets of parameters and provide them to functions instead of parameter-value pairs, and save the structs to MAT-files for later use.

The surface function plots the surface into the axes. Setting the Parent of the surface to hgrotate puts the surface object under the control of the hgtransform that spins the globe (see the illustration in Orient the Globe and Graticule). By setting EdgeColor to 'none', the globe displays face colors only, with no grid lines (which, by default, display in black). The colormap function sets the colormap for the surface to the 64-by-3 colormap cmap defined in the code, which is appropriate for terrain display. While you can use more colors, 64 is sufficient, given the relative coarseness of the texture map (1-by-1 degree resolution).

Plot the Graticule

Unlike the globe grid, the graticule grid displays with no face colors and gray edge color. (You turn the graticule grid on and off by clicking the Show grid button.) Like the terrain map, it is a surfaceplot object; however, the mesh function creates it, rather than the surface function, as follows:

hmesh = mesh(gx,gy,gz,'parent',hgrotate,...
        'FaceColor','none','EdgeColor',[.5 .5 .5]);
set(hmesh,'Visible','off')

The state of the Show grid button is initially off, causing the graticule not to display. Show grid toggles the mesh object's Visible property.

As mentioned earlier, enlarging the graticule by 2 percent before plotting prevents the terrain surface from obscuring it.

Orient the Globe and Graticule

The globe and graticule rotate as if they were one object, under the control of a pair of hgtransform objects. Within the figure, the HG objects are set up in this hierarchy.

The tilt transform applies a rotation about the x-axis of 0.5091 radians (equal to 23.44 degrees, the inclination of the Earth's axis of rotation). The rotate transform initially has a default identity matrix. The spinstopbutton_Callback subsequently updates the matrix to rotate about the z-axis by 0.01745329252 radians (1 degree) per iteration, using the following code:

az = az + 0.01745329252;
set(hgrotate,'Matrix',makehgtform('zrotate',az));
drawnow                  % Refresh the screen

Light the Globe and Shift the Light Source

A light object illuminates the globe, initially from the left. Two sliders control the light's position, which you can manipulate whether the globe is standing still or rotating. The light is a child of the axes, so is not affected by either of the hgtransforms. The call to light uses no parameters other than its altitude and an azimuth:

hlight = camlight(0,0);

After creating the light, the axes1_CreateFcn adds some handles and data that other callbacks need to the handles structure:

handles.light = hlight;
handles.tform = hgrotate;
handles.hmesh = hmesh;
handles.azimuth = 0.;
handles.cmap = cmap;
guidata(gcf,handles);

The call to guidata caches the data added to handles.

Moving either of the sliders sets both the elevation and the azimuth of the light source, although each slider changes only one. The code in the callback for varying the elevation of the light is

function sunelslider_Callback(hObject, eventdata, handles)

hlight = handles.light;                   % Get handle to light object
sunaz = get(handles.sunazslider,'value'); % Get current light azimuth
sunel = get(hObject,'value');             % Varies from -72.8 -> 72.8 deg
lightangle(hlight,sunaz,sunel)            % Set the new light angle

The callback for the light azimuth slider works similarly, querying the elevation slider's setting to keep that value from being changed in the call to lightangle.

Was this topic helpful?