Animation class
Contents
Description
This is a handle class responsible for creating, displaying and saving an animation of a required result.
Three types of animated results are available:
- Motion: Shows only the motion of particles and walls.
- Scalar: Shows the motion of the elements and fills the particles with a color scale to represent a scalar result.
- Vector: Shows the motion of the elements and uses arrows to indicate the direction and intensity of a vector result.
classdef Animation < handle
Constant values: animation types
properties (Constant = true, Access = public) MOTION = uint8(1); % Particles motion with no result indication SCALAR = uint8(2); % Particles with color to indicate scalar result VECTOR = uint8(3); % Particles with arrow to indicate vector result end
Constant values: default drawing properties
properties (Constant = true, Access = public) % Colors col_pedge = char('k'); % color of particle edge col_pfill = char('w'); % color of particle interior when not showing scalar result col_wall = char('k'); % color of wall when not showing scalar result col_bbox = char('b'); % color of bounding box limit col_sink = char('r'); % color of sink limit % Line widths wid_pedge = double(0.1); % width of particle edge wid_wall = double(2.0); % width of wall wid_bbox = double(0.5); % width of bounding box wid_sink = double(0.5); % width of sink % Line styles sty_pedge = char('-'); % style of particle edge sty_wall = char('-'); % style of wall sty_bbox = char('--'); % style of bounding box sty_sink = char('--'); % style of sink % Particle ID size id_size = double(8); end
Public properties
properties (SetAccess = public, GetAccess = public) % Identification res_type uint8 = uint8.empty; % flag for result type anim_type uint8 = uint8.empty; % flag for animation type anim_title string = string.empty; % animation title % Results: motion (common to all animation types) times double = double.empty; % array of simulation times of each step coord_x double = double.empty; % array of particles x coordinates coord_y double = double.empty; % array of particles y coordinates radius double = double.empty; % array of particles radius wall_pos double = double.empty; % array of wall positions % Results: scalar res_part double = double.empty; % array of scalar result to be exhibited for particles res_wall double = double.empty; % array of scalar result to be exhibited for walls res_range double = double.empty; % array of results range (minimum to maximum value) col_range double = double.empty; % array of colorbar range (minimum to maximum value) % Results: vector res_vecx double = double.empty; % array of x vector result to be exhibited res_vecy double = double.empty; % array of y vector result to be exhibited arrow_fct double = double.empty; % Multiplication factor to define vetor arrow size % Options play logical = logical.empty; % flag for playing animation in Matlab after creation pids logical = logical.empty; % flag for ploting particles IDs bbox double = double.empty; % fixed limits for animation % Animation components fig matlab.ui.Figure = matlab.ui.Figure.empty; % figure handle frames struct = struct('cdata',[],'colormap',[]); % array of movie frames fps double = double.empty; % movie frame rate end
Constructor method
methods function this = Animation() this.setDefaultProps(); end end
Public methods: managing methods
methods %------------------------------------------------------------------ function setDefaultProps(this) this.play = true; this.pids = false; end %------------------------------------------------------------------ function curConfig(this,drv,prefix) % Create figure f = figure('name',[prefix,' ','Configuration']); % Set figure properties f.Visible = 'off'; f.Units = 'normalized'; f.Position = [0.1 0.1 0.8 0.8]; axis(gca,'equal'); hold on; % Get last stored results col = drv.result.idx; this.times = drv.result.times(col); this.coord_x = drv.result.coord_x(:,col); this.coord_y = drv.result.coord_y(:,col); this.radius = drv.result.radius(:,col); this.wall_pos = drv.result.wall_position(:,col); % Check if there are results at current time if (isnan(this.times)) close f; return; end % Set axes limits if (~isempty(this.bbox)) xlim(this.bbox(1:2)) ylim(this.bbox(3:4)) else this.setBBoxCur(drv); end % Get default result to show for each type of analysis if (drv.type == drv.MECHANICAL) title(gca,[prefix,' ',sprintf('Configuration - Time: %.3f',drv.time)]); this.anim_type = this.MOTION; this.drawFrame(drv,1); else title(gca,[prefix,' ',sprintf('Temperatures - Time: %.3f',drv.time)]); this.anim_type = this.SCALAR; % Get last stored temperatures (must be available) if (~isempty(drv.result.temperature(:,col))) this.res_part = drv.result.temperature(:,col); end if (~isempty(drv.result.wall_temperature(:,col))) this.res_wall = drv.result.wall_temperature(:,col); end % Set result and colorbar ranges (always automatic for current configuration) this.setRange(); % Draw model components this.drawFrame(drv,1); end % Show figure f.Visible = 'on'; pause(1); end %------------------------------------------------------------------ function animate(this,drv) % Create new figure this.fig = figure('Name',this.anim_title); % Set figure properties this.fig.Visible = 'off'; this.fig.Units = 'normalized'; this.fig.Position = [0.1 0.1 0.8 0.8]; axis(gca,'equal'); hold on; % Set motion results (always needed to show model animation) this.times = drv.result.times; this.coord_x = drv.result.coord_x; this.coord_y = drv.result.coord_y; this.radius = drv.result.radius; this.wall_pos = drv.result.wall_position; % Set axes limits if (~isempty(this.bbox)) xlim(this.bbox(1:2)) ylim(this.bbox(3:4)) else this.setBBoxAll(drv); end % Set type and create animation this.setType(drv) this.createAnimation(drv); % Save movie file drv.createOutFolder(); file_name = strcat(drv.path_out,this.anim_title); writer = VideoWriter(file_name,'MPEG-4'); writer.FrameRate = this.fps; open(writer); writeVideo(writer,this.frames); close(writer) end %------------------------------------------------------------------ function createAnimation(this,drv) if (this.anim_type == this.SCALAR) this.setRange(); elseif (this.anim_type == this.VECTOR) this.setArrowSize(); end % Get total number of valid movie frames nf = drv.result.idx; if (isnan(this.times(nf))) nf = find(isnan(this.times)) - 1; end % Preallocate movie frames array frams(nf) = struct('cdata',[],'colormap',[]); % Create waitbar wb = waitbar(0.0,{sprintf('Creating animation "%s"',this.anim_title),'0.0%'},... 'Name','Animation Creation',... 'CreateCancelBtn','setappdata(gcbf,''canceling'',1)'); setappdata(wb,'canceling',0); % Generate movie frames (one for each results storage time) tim = this.times(1:nf); tit = this.anim_title; for i = 1:nf % Draw frame set(0,'CurrentFigure',this.fig) cla; title(gca,strcat(tit,sprintf(' - Time: %.3f',tim(i)))); this.drawFrame(drv,i); frams(i) = getframe(this.fig); % Update waitbar prog = double(i)/double(nf); waitbar(prog,wb,{sprintf('Creating animation "%s"',this.anim_title),... sprintf('%.1f%%',100*prog)}); if getappdata(wb,'canceling') break end end delete(wb); % Get generated frames this.frames = frams(1:i); % Compute frame rate (frames / second) this.fps = ceil(nf/drv.time); if (this.fps > 100) this.fps = 100; elseif (this.fps < 1) this.fps = 1; end end %------------------------------------------------------------------ function showAnimation(this) if(this.play) fprintf('\nShowing animation "%s"...',this.anim_title); this.fig.Visible = 'on'; movie(this.fig,this.frames,10,this.fps); end end %------------------------------------------------------------------ function drawFrame(this,drv,f) % Draw particles this.drawParticles(f); % Draw walls for i = 1:drv.n_walls this.drawWall(drv.walls(i),f); end % Draw bounding box if (drv.has_bbox) if (drv.bbox.isActive(this.times(f))) this.drawBBox(drv.bbox); end end % Draw sinks for i = 1:length(drv.sink) if (drv.sink(i).isActive(this.times(f))) this.drawSink(drv.sink(i)); end end end %------------------------------------------------------------------ function drawParticles(this,f) % Loop over particles through the length of radius results % (not drv.n_particles because it is the current number of particles) for i = 1:size(this.radius,1) % Check if particle exists at this frame time % (radius and coords should always be available for existing particles) if (isnan(this.radius(i,f)) || isnan(this.coord_x(i,f)) || isnan(this.coord_y(i,f))) continue; end % Draw particle with selected result % (check if particle exists by looking at the required result) switch this.anim_type case this.MOTION % Allow to pass when result is empty since this is % the case when drawing initial configuration % (orientation result is checked inside) if (~isempty(this.res_part) && isnan(this.res_part(i,f))) continue; end this.drawParticleMotion(i,f); case this.SCALAR if (isnan(this.res_part(i,f))) continue; end this.drawParticleScalar(i,f); case this.VECTOR if (isnan(this.res_vecx(i,f)) || isnan(this.res_vecy(i,f))) continue; end this.drawParticleVector(i,f); end % Show ID number if (this.pids &&... ~isnan(this.coord_x(i,f)) && ~isnan(this.coord_y(i,f))) x = this.coord_x(i,f); y = this.coord_y(i,f); text(x,y,int2str(i),'FontSize',this.id_size); end end end end
Public methods: auxiliary methods
methods %------------------------------------------------------------------ function setType(this,drv) switch this.res_type % Motion case drv.result.MOTION this.anim_type = this.MOTION; this.res_part = drv.result.orientation; % Scalar case drv.result.RADIUS this.anim_type = this.SCALAR; this.res_part = drv.result.radius; case drv.result.MASS this.anim_type = this.SCALAR; this.res_part = drv.result.mass; case drv.result.COORDINATE_X this.anim_type = this.SCALAR; this.res_part = drv.result.coord_x; case drv.result.COORDINATE_Y this.anim_type = this.SCALAR; this.res_part = drv.result.coord_y; case drv.result.ORIENTATION this.anim_type = this.SCALAR; this.res_part = drv.result.orientation; case drv.result.FORCE_MOD this.anim_type = this.SCALAR; x = drv.result.force_x; y = drv.result.force_y; this.res_part = sqrt(x.^2+y.^2); case drv.result.FORCE_X this.anim_type = this.SCALAR; this.res_part = drv.result.force_x; case drv.result.FORCE_Y this.anim_type = this.SCALAR; this.res_part = drv.result.force_y; case drv.result.TORQUE this.anim_type = this.SCALAR; this.res_part = drv.result.torque; case drv.result.VELOCITY_MOD this.anim_type = this.SCALAR; x = drv.result.velocity_x; y = drv.result.velocity_y; this.res_part = sqrt(x.^2+y.^2); case drv.result.VELOCITY_X this.anim_type = this.SCALAR; this.res_part = drv.result.velocity_x; case drv.result.VELOCITY_Y this.anim_type = this.SCALAR; this.res_part = drv.result.velocity_y; case drv.result.VELOCITY_ROT this.anim_type = this.SCALAR; this.res_part = drv.result.velocity_rot; case drv.result.ACCELERATION_MOD this.anim_type = this.SCALAR; x = drv.result.acceleration_x; y = drv.result.acceleration_y; this.res_part = sqrt(x.^2+y.^2); case drv.result.ACCELERATION_X this.anim_type = this.SCALAR; this.res_part = drv.result.acceleration_x; case drv.result.ACCELERATION_Y this.anim_type = this.SCALAR; this.res_part = drv.result.acceleration_y; case drv.result.ACCELERATION_ROT this.anim_type = this.SCALAR; this.res_part = drv.result.acceleration_rot; case drv.result.TEMPERATURE this.anim_type = this.SCALAR; this.res_part = drv.result.temperature; this.res_wall = drv.result.wall_temperature; case drv.result.HEAT_RATE this.anim_type = this.SCALAR; this.res_part = drv.result.heat_rate; % Vector case drv.result.FORCE_VEC this.anim_type = this.VECTOR; this.res_vecx = drv.result.force_x; this.res_vecy = drv.result.force_y; case drv.result.VELOCITY_VEC this.anim_type = this.VECTOR; this.res_vecx = drv.result.velocity_x; this.res_vecy = drv.result.velocity_y; case drv.result.ACCELERATION_VEC this.anim_type = this.VECTOR; this.res_vecx = drv.result.acceleration_x; this.res_vecy = drv.result.acceleration_y; end end %------------------------------------------------------------------ % Set the Animation BBox according to current existing particles. function setBBoxCur(~,drv) % Initialize axes limits if (drv.n_particles ~= 0 || drv.n_walls ~= 0) xmin = inf; ymin = inf; xmax = -inf; ymax = -inf; else xmin = -1; ymin = -1; xmax = 1; ymax = 1; end % Compute axes limits for i = 1:drv.n_particles [xmin_p,ymin_p,xmax_p,ymax_p] = drv.particles(i).getBBoxLimits(); xmin = min(xmin,xmin_p); ymin = min(ymin,ymin_p); xmax = max(xmax,xmax_p); ymax = max(ymax,ymax_p); end for i = 1:drv.n_walls [xmin_w,ymin_w,xmax_w,ymax_w] = drv.walls(i).getBBoxLimits(); xmin = min(xmin,xmin_w); ymin = min(ymin,ymin_w); xmax = max(xmax,xmax_w); ymax = max(ymax,ymax_w); end % Create margin dx = xmax-xmin; dy = ymax-ymin; if (dx == 0 && dy == 0) xmin = -1; ymin = -1; xmax = 1; ymax = 1; else if (dx == 0) dx = dy; end if (dy == 0) dy = dx; end end xmin = xmin - dx/10; ymin = ymin - dy/10; xmax = xmax + dx/10; ymax = ymax + dy/10; % Set axes limits xlim([xmin,xmax]) ylim([ymin,ymax]) end %------------------------------------------------------------------ % Set the Animation BBox according to coordinates history. function setBBoxAll(~,drv) % Extreme particle centroid coordinates xmin_p = min(min(drv.result.coord_x)); xmax_p = max(max(drv.result.coord_x)); ymin_p = min(min(drv.result.coord_y)); ymax_p = max(max(drv.result.coord_y)); % Consider radius size rmax = max(max(drv.result.radius)); xmin_p = xmin_p - rmax; xmax_p = xmax_p + rmax; ymin_p = ymin_p - rmax; ymax_p = ymax_p + rmax; % Compute extreme wall coordinates xmin_w = inf; xmax_w = -inf; ymin_w = inf; ymax_w = -inf; for i = 1:drv.n_walls row = 4 * (drv.walls(i).id-1) + 1; if (drv.walls(i).type == drv.walls(i).LINE) xmin_ww = min(min(drv.result.wall_position([row+0,row+2],:))); xmax_ww = max(max(drv.result.wall_position([row+0,row+2],:))); ymin_ww = min(min(drv.result.wall_position([row+1,row+3],:))); ymax_ww = max(max(drv.result.wall_position([row+1,row+3],:))); elseif (drv.walls(i).type == drv.walls(i).CIRCLE) xmin_ww = min(drv.result.wall_position(row+0,:)-drv.result.wall_position(row+2,:)); xmax_ww = max(drv.result.wall_position(row+0,:)+drv.result.wall_position(row+2,:)); ymin_ww = min(drv.result.wall_position(row+1,:)-drv.result.wall_position(row+2,:)); ymax_ww = max(drv.result.wall_position(row+1,:)+drv.result.wall_position(row+2,:)); end if (xmin_ww < xmin_w) xmin_w = xmin_ww; end if (xmax_ww > xmax_w) xmax_w = xmax_ww; end if (ymin_ww < ymin_w) ymin_w = ymin_ww; end if (ymax_ww > ymax_w) ymax_w = ymax_ww; end end % Get total min/max coordinates xmin = min(xmin_p,xmin_w); xmax = max(xmax_p,xmax_w); ymin = min(ymin_p,ymin_w); ymax = max(ymax_p,ymax_w); % Create margin dx = xmax-xmin; dy = ymax-ymin; if (dx == 0 && dy == 0) xmin = -1; ymin = -1; xmax = 1; ymax = 1; else if (dx == 0) dx = dy; end if (dy == 0) dy = dx; end end xmin = xmin - dx/10; ymin = ymin - dy/10; xmax = xmax + dx/10; ymax = ymax + dy/10; % Set axes limits xlim([xmin,xmax]) ylim([ymin,ymax]) end %------------------------------------------------------------------ function [min_val,max_val] = scalarValueLimits(this) % Limits of particles results if (~isempty(this.res_part)) min_val_p = min(this.res_part(:)); max_val_p = max(this.res_part(:)); else min_val_p = inf; max_val_p = -inf; end % Limits of walls results if (~isempty(this.res_wall)) min_val_w = min(this.res_wall(:)); max_val_w = max(this.res_wall(:)); else min_val_w = inf; max_val_w = -inf; end % Total limits of results min_val = min([min_val_p,min_val_w]); max_val = max([max_val_p,max_val_w]); % Treat inf and NaN values if ((isinf(min_val) && isinf(max_val)) || (isnan(min_val) && isnan(max_val))) min_val = -1; max_val = +1; elseif (isinf(min_val) || isnan(min_val)) min_val = max_val - 1; elseif (isinf(max_val) || isnan(max_val)) max_val = min_val + 1; end % Treat equal values if (min_val == max_val) min_val = min_val - 1; max_val = max_val + 1; end end %------------------------------------------------------------------ function setRange(this) % Compute result limits [min_val,max_val] = this.scalarValueLimits(); % Set result range if (isempty(this.res_range)) this.res_range = [min_val,max_val]; end % Set colorbar range this.col_range = linspace(this.res_range(1),this.res_range(2),256); colormap jet; caxis([this.res_range(1),this.res_range(2)]); colorbar; end %------------------------------------------------------------------ function setArrowSize(this) % Maximum vector norm value x = this.res_vecx; y = this.res_vecy; max_vec = max(max(sqrt(x.^2+y.^2))); % Figure size limitx = xlim; limity = ylim; dx = limitx(2)-limitx(1); dy = limity(2)-limity(1); d = sqrt(dx^2+dy^2); % Multiplication factor if (max_vec ~= 0 && ~isnan(max_vec)) this.arrow_fct = 0.1 * d/max_vec; else this.arrow_fct = 0; end end end
Public methods: plotting methods
methods %------------------------------------------------------------------ function drawParticleMotion(this,i,j) % Always check valid data for safety if (isnan(this.radius(i,j)) || isnan(this.coord_x(i,j)) || isnan(this.coord_y(i,j))) return; end % Position x = this.coord_x(i,j); y = this.coord_y(i,j); r = this.radius(i,j); pos = [x-r,y-r,2*r,2*r]; % Plot particle (rectangle with rounded sides) c = this.col_pedge; w = this.wid_pedge; s = this.sty_pedge; rectangle('Position',pos,'Curvature',[1 1],'EdgeColor',c,'LineWidth',w,'LineStyle',s,'FaceColor','none'); % Plot orientation (default particle result of motion animations) if (~isempty(this.res_part) && ~isnan(this.res_part(i,j))) o = this.res_part(i,j); p = [x,y]+[r*cos(o),r*sin(o)]; line([x,p(1)],[y,p(2)],'Color',c,'LineWidth',w,'LineStyle',s); end end %------------------------------------------------------------------ function drawParticleScalar(this,i,j) % Always check valid data for safety if (isnan(this.radius(i,j)) || isnan(this.coord_x(i,j)) || isnan(this.coord_y(i,j))) return; end % Position x = this.coord_x(i,j); y = this.coord_y(i,j); r = this.radius(i,j); rr = this.res_range; cr = this.col_range; pos = [x-r,y-r,2*r,2*r]; % Color according to result if (~isempty(this.res_part) &&... all(this.res_part(i,j) >= rr(1) & this.res_part(i,j) <= rr(2))) clr = interp1(cr,colormap,this.res_part(i,j)); else clr = this.col_pfill; end % Plot particle (rectangle with rounded sides) c = this.col_pedge; w = this.wid_pedge; s = this.sty_pedge; rectangle('Position',pos,'Curvature',[1 1],'EdgeColor',c,'LineWidth',w,'LineStyle',s,'FaceColor',clr); end %------------------------------------------------------------------ function drawParticleVector(this,i,j) % Always check valid data for safety if (isnan(this.radius(i,j)) || isnan(this.coord_x(i,j)) || isnan(this.coord_y(i,j))) return; end % Position x = this.coord_x(i,j); y = this.coord_y(i,j); r = this.radius(i,j); pos = [x-r,y-r,2*r,2*r]; % Plot particle (rectangle with rounded sides) c = this.col_pedge; w = this.wid_pedge; s = this.sty_pedge; rectangle('Position',pos,'Curvature',[1 1],'EdgeColor',c,'LineWidth',w,'LineStyle',s,'FaceColor','none'); % Plot vector arrow if (~isempty(this.res_vecx) && ~isempty(this.res_vecy) &&... ~isnan(this.res_vecx(i,j)) && ~isnan(this.res_vecy(i,j))) vx = this.arrow_fct * this.res_vecx(i,j); vy = this.arrow_fct * this.res_vecy(i,j); line([x,x+vx],[y,y+vy],'Color',c,'LineWidth',w,'LineStyle',s); end end %------------------------------------------------------------------ function drawWall(this,wall,j) w = this.wid_wall; s = this.sty_wall; rr = this.res_range; cr = this.col_range; % Color according to result if (~isempty(this.res_wall) &&... ~wall.insulated &&... all(this.res_wall(wall.id,j) >= rr(1) & this.res_wall(wall.id,j) <= rr(2))) c = interp1(cr,colormap,this.res_wall(wall.id,j)); else c = this.col_wall; end % Row ID row = 4 * (wall.id-1) + 1; switch wall.type case wall.LINE x1 = this.wall_pos(row+0,j); y1 = this.wall_pos(row+1,j); x2 = this.wall_pos(row+2,j); y2 = this.wall_pos(row+3,j); line([x1,x2],[y1,y2],'Color',c,'LineWidth',w,'LineStyle',s); case wall.CIRCLE x = this.wall_pos(row+0,j); y = this.wall_pos(row+1,j); r = this.wall_pos(row+2,j); this.drawCircle(x,y,r,c,w,s); end end %------------------------------------------------------------------ function drawBBox(this,bbox) c = this.col_bbox; w = this.wid_bbox; s = this.sty_bbox; switch bbox.type case bbox.RECTANGLE x1 = bbox.limit_min(1); y1 = bbox.limit_min(2); x2 = bbox.limit_max(1); y2 = bbox.limit_max(2); if (isinf(x1)) ax = gca; x1 = ax.XLim(1); end if (isinf(y1)) ax = gca; y1 = ax.YLim(1); end if (isinf(x2)) ax = gca; x2 = ax.XLim(2); end if (isinf(y2)) ax = gca; y2 = ax.YLim(2); end pos = [x1,y1,x2-x1,y2-y1]; rectangle('Position',pos,'EdgeColor',c,'LineWidth',w,'LineStyle',s); case bbox.CIRCLE if (~isinf(bbox.radius)) r = bbox.radius; else r = 0; end this.drawCircle(bbox.center(1),bbox.center(2),r,c,w,s); case bbox.POLYGON pol = polyshape(bbox.coord_x,bbox.coord_y); plot(pol,'FaceColor','none','EdgeColor',c,'LineWidth',w,'LineStyle',s); end end %------------------------------------------------------------------ function drawSink(this,sink) c = this.col_sink; w = this.wid_sink; s = this.sty_sink; switch sink.type case sink.RECTANGLE x1 = sink.limit_min(1); y1 = sink.limit_min(2); x2 = sink.limit_max(1); y2 = sink.limit_max(2); if (isinf(x1)) ax = gca; x1 = ax.XLim(1); end if (isinf(y1)) ax = gca; y1 = ax.YLim(1); end if (isinf(x2)) ax = gca; x2 = ax.XLim(2); end if (isinf(y2)) ax = gca; y2 = ax.YLim(2); end pos = [x1,y1,x2-x1,y2-y1]; rectangle('Position',pos,'EdgeColor',c,'LineWidth',w,'LineStyle',s); case sink.CIRCLE if (~isinf(sink.radius)) r = sink.radius; else r = 0; end this.drawCircle(sink.center(1),sink.center(2),r,c,w,s); case sink.POLYGON pol = polyshape(sink.coord_x,sink.coord_y); plot(pol,'FaceColor','none','EdgeColor',c,'LineWidth',w,'LineStyle',s); end end %------------------------------------------------------------------ function h = drawCircle(~,x,y,r,c,w,s) t = 0:pi/50:2*pi; x_circle = x + r * cos(t); y_circle = y + r * sin(t); h = plot(x_circle,y_circle,'Color',c,'LineWidth',w,'LineStyle',s); end end
end