I am trying to make a user interactive animation in matlab. It is a square that translates and rotates across the screen and the user has to click on it. If they click on it, they will receive points and the animation will repeat. If they click on the whitespace (eg. anywhere except within the square) The animation will exit and YOU LOSE will be displayed. I have the animation almost finished using two functions. One to create the animation and another that registers the mouse click. So far I can recognize the mouse click and the animation will stop if the user clicks on the whitespace but the animation will not repeat if the user clicks within the polygon. I am unsure how to modify my code so that the animation will repeat until the user clicks the white space. I have pasted my code below. Any help would be greatly appreciated.
Animation function:
function movingPolygon
global gUserHitPolygon;
global gCurrentXVertices;
global gCurrentYVertices;
gUserHitPolygon = true;
nSides =4;
%Polar points
r=1;
theta = pi/nSides * (1:2:2*nSides-1);
%Cartesisn points
x0 = r * cos(theta);
y0 = r * sin(theta);
nFrames = 100;
xx = linspace(0,10, nFrames);
yy = xx;
rr = linspace(0, 2*pi, nFrames);
h = figure;
set(h,'WindowButtonDownFcn', #mouseDownCallback);
for i = 1:nFrames
rX = [cos(rr(i)), -sin(rr(i))];
rY = [sin(rr(i)), cos(rr(i))];
x1 = rX * [x0; y0];
y1 = rY * [x0; y0];
x2= x1 + xx(i);
y2= y1 + yy(i);
gCurrentXVertices=x2;
gCurrentYVertices=y2;
y=fill(x2, y2, 'b');
xlim([0,10]); ylim([0,10]);
hold on;
pause(0.000000003);
if ~gUserHitPolygon
clear GLOBAL gUserHitPolygon gCurrentXVertices gCurrentYVertices;
break;
end
delete(y);
end
end
Callback Function:
function mouseDownCallback(~,~)
global UserHitPolygon;
global CurrentXVertices;
global CurrentYVertices;
xVertices = gCurrentXVertices;
yVertices = gCurrentYVertices;
% if we have valid (so non-empty) sets of x- and y-vertices then...
if ~isempty(xVertices) && ~isempty(yVertices)
% get the coordinate on the current axis
coordinates = get(gca,'CurrentPoint');
coordinates = coordinates(1,1:2);
% if the coordinate is not in the polygon, then change the
% flag
if ~inpolygon(coordinates(1),coordinates(2),xVertices,yVertices)
gUserHitPolygon = false;
end
end
end
Edit: fixed some bugs in the callback function.
Short answer
include your animation in a while loop
Long Answer
No matter what a user does, the animation will only play once, because it doesn't know to repeat. The answer to this is to use a while loop, which will repeat your animation until you tell it to stop. Your main loop then becomes
done = false; % your stopping condition
while ~done
for i = 1:nFrames
rX = [cos(rr(i)), -sin(rr(i))];
rY = [sin(rr(i)), cos(rr(i))];
x1 = rX * [x0; y0];
y1 = rY * [x0; y0];
x2= x1 + xx(i);
y2= y1 + yy(i);
gCurrentXVertices=x2;
gCurrentYVertices=y2;
y=fill(x2, y2, 'b');
xlim([0,10]); ylim([0,10]);
hold on;
pause(0.000000003);
if ~gUserHitPolygon
clear GLOBAL gUserHitPolygon gCurrentXVertices gCurrentYVertices;
done = true; % set your stopping condition
break;
end
delete(y);
end
end
Related
I have a 3D figure in Matlab, let's assume it's sphere. What I need, is to get the X, Y, Z values of the point on surface, that I click with the mouse.
r = 10;
[X,Y,Z] = sphere(50);
X2 = X * r;
Y2 = Y * r;
Z2 = Z * r;
figure();
props.FaceColor = 'texture';
props.EdgeColor = 'none';
props.FaceLighting = 'phong';
sphere = surf(X2,Y2,Z2,props);
axis equal
hold on
clicked_point = [?,?,?];
So in this example I want the clicked_point to be equal to [-3.445,-7.32,5.878].
I've tried with such solution:
clear all;
close all;
r = 10;
[X,Y,Z] = sphere(50);
X2 = X * r;
Y2 = Y * r;
Z2 = Z * r;
fig = figure();
props.FaceColor = 'texture';
props.EdgeColor = 'none';
props.FaceLighting = 'phong';
sphere = surf(X2,Y2,Z2,props);
axis equal
dcm_obj = datacursormode(fig);
set(dcm_obj,'DisplayStyle','datatip',...
'SnapToDataVertex','off','Enable','on')
c_info = getCursorInfo(dcm_obj);
while length(c_info) < 1
datacursormode on
c_info = getCursorInfo(dcm_obj);
end
But after that I can't even click on the sphere to display any data on figure. How can I get X, Y, Z in script? If not, how can I detect that the mouse click has already occured in Matlab?
It is not clear whether you want the clicked_point variable to reside in the base workspace or if that is going to be part of a GUI.
I'll give you a solution for the base workspace.
The trick is to just add the bit of code you need to the UpdateFcn of the datacursormode object.
Save a function getClickedPoint.m somewhere visible on your MATLAB path:
function output_txt = getClickedPoint(obj,event_obj)
% Display the position of the data cursor
% obj Currently not used (empty)
% event_obj Handle to event object
% output_txt Data cursor text string (string or cell array of strings).
pos = get(event_obj,'Position');
output_txt = {['X: ',num2str(pos(1),4)],...
['Y: ',num2str(pos(2),4)]};
% If there is a Z-coordinate in the position, display it as well
if length(pos) > 2
output_txt{end+1} = ['Z: ',num2str(pos(3),4)];
end
assignin('base','clicked_point',pos)
All this code is actually a copy of the default function used by data cursors. The only modifications are:
I changed the name (obviously you want it to be unique)
I added the last line of code
This last line of code use assignin to transfer the position of the cursor into a variable (named clicked_point) in the base workspace.
Armed with that, keep your code which generate the sphere (although I recommend you change the name of the surface object to something else than sphere as this is a built in MATLAB function), and we just have to modify the datacursormode object to instruct it to use our getClickedPoint function:
[X,Y,Z] = sphere(50);
r = 10 ; X2 = X * r; Y2 = Y * r; Z2 = Z * r;
fig = figure ;
hs = surf(X2,Y2,Z2,'FaceColor','texture','EdgeColor','none','FaceLighting','phong');
axis equal
%% Assign custom update function to dcm
dcm_obj = datacursormode(fig);
set(dcm_obj,'SnapToDataVertex','off','Enable','on','UpdateFcn',#getClickedPoint)
Now the first time you click on the sphere, the variable clicked_point will be created in the workspace with the coordinates of the point. And every time you click again on the sphere the variable will be updated:
If this is to be applied with a GUI, use the same technique but instead of assignin, I would recommend to use the setappdata function. (you can read Share Data Among Callbacks to have details about how this works.)
I am trying to change the radius of a sphere by using left and right arrow keys, plotted in a MATLAB figure. For example by pressing right arrow, the value of radius increases by one and then plot the new sphere with the updated radius. Similarly, pressing left arrow key decreases the radius by one and then plots a smaller sphere. However, I want this change to be limited between 1 and rmax.
I got some ideas of how I can approach after reading this post but still it is not what I am looking for. Therefore, I used two global variables to achieve this task in order to somehow pass the information by reference to KeyPressFcn so when a key is pressed KeyPressFcn know what those limits are. In the example below, the code does increase and decrease the radius by one but it is it does not restrict the change of radius within the specified range after left and right arrows are hit.
Is there a better way of approaching this? How can I pass the value of radius and its limits to KeyPressFcn? I want KeyPressFcn to know how much it can change the radius when left and right arrows are pressed.
function animateme()
fig_h = figure;
set(fig_h,'KeyPressFcn', #key_pressed_fcn);
global r rmax
p0 = [0 0 0];
[x,y,z] = sphere;
rmax = 10;
r = 1;
while 1==1
h = surf(x*r+p0(1), y*r+p0(2), z*r+p0(3));
set(h, 'FaceAlpha', 0.5, 'FaceColor', rand([1 3]))
axis equal;
pause
end
function key_pressed_fcn(fig_obj, eventDat)
global r rmax
if strcmpi(eventDat.Key, 'rightarrow')
r = r + 1;
if r < 1
r = 1;
end
elseif strcmpi(eventDat.Key, 'leftarrow')
r = r - 1;
if r > rmax
r = rmax;
end
end
disp(r)
First of all, don't use global variables as there is (almost) always a better way of accomplishing the same thing.
Here is an example by using nested functions which automatically have access to the variables within the parent function's workspace.
function animateme()
fig = figure();
hax = axes('Parent', fig);
set(fig, 'KeyPressFcn', #keypress)
p0 = [0,0,0];
[x,y,z] = sphere();
% Specify limits here which are accessible to nested functions
rmax = 10;
r = 1;
h = surf(x,y,z, 'Parent', hax);
% Subfunction for re-plotting the data
% This prevents you from needing a while loop
function redraw()
set(h, 'XData', x * r + p0(1), ...
'YData', y * r + p0(2), ...)
'ZData', z * r + p0(3));
set(h, 'FaceAlpha', 0.5, ...
'FaceColor', rand([1 3]))
axis(hax, 'equal')
drawnow
end
% Go ahead and do the first redraw
redraw();
% Callback to process keypress events
function keypress(~, evnt)
switch lower(evnt.Key)
case 'rightarrow'
r = min(r + 1, rmax);
case 'leftarrow'
r = max(1, r - 1);
otherwise
return
end
% Always do a redraw
redraw();
end
end
Another option is to store the current value of r within the graphics objects themselves using the UserData field. So you could put it in the surf plot itself. This is actually my preferred method because then your callback function can live anywhere and still have access to the data it needs.
function animateme()
fig = figure();
% Data to store for plotting
data.p = [0,0,0];
data.r = 1;
data.rmax = 10;
% Create a blank surface for starters
h = surf(nan(2), nan(2), nan(2));
set(h, 'UserData', data);
% Update the display of the surface
redraw(h);
% Set the callback and pass the surf handle
set(fig, 'KeyPressFcn', #(fig, evnt)keypress(h, evnt))
end
function redraw(h)
% Get the stored data from the graphics object
userdata = get(h, 'Userdata');
[x,y,z] = sphere();
set(h, 'XData', x * userdata.r + userdata.p(1), ...
'YData', y * userdata.r + userdata.p(2), ...
'ZData', z * userdata.r + userdata.p(3));
set(h, 'FaceAlpha', 0.5, ...
'FaceColor', rand([1 3]))
axis equal
drawnow;
end
function keypress(h, evnt)
% Get the stored data
userdata = get(h, 'Userdata');
switch lower(evnt.Key)
case 'rightarrow'
userdata.r = min(userdata.r + 1, userdata.rmax);
case 'leftarrow'
userdata.r = max(1, userdata.r - 1);
otherwise
return;
end
% Update the stored value
set(h, 'UserData', userdata);
redraw(h);
end
I have to create some draggable points on an axes. However, this seems to be a very slow process, on my machine taking a bit more than a second when done like so:
x = rand(100,1);
y = rand(100,1);
tic;
for i = 1:100
h(i) = impoint(gca, x(i), y(i));
end
toc;
Any ideas on speed up would be highly appreciated.
The idea is simply to provide the user with the possibility to correct positions in a figure that have been previously calculated by Matlab, here exemplified by the random numbers.
You can use the the ginput cursor within a while loop to mark all points you want to edit. Afterwards just click outside the axes to leave the loop, move the points and accept with any key.
f = figure(1);
scatter(x,y);
ax = gca;
i = 1;
while 1
[u,v] = ginput(1);
if ~inpolygon(u,v,ax.XLim,ax.YLim); break; end;
[~, ind] = min(hypot(x-u,y-v));
h(i).handle = impoint(gca, x(ind), y(ind));
h(i).index = ind;
i = i + 1;
end
Depending on how you're updating your plot you can gain a general speedup by using functions like clf (clear figure) and cla (clear axes) instead of always opening a new figure window as explained in this answer are may useful.
Alternatively the following is a very rough idea of what I meant in the comments. It throws various errors and I don't have the time to debug it right now. But maybe it helps as a starting point.
1) Conventional plotting of data and activating of datacursormode
x = rand(100,1);
y = rand(100,1);
xlim([0 1]); ylim([0 1])
f = figure(1)
scatter(x,y)
datacursormode on
dcm = datacursormode(f);
set(dcm,'DisplayStyle','datatip','Enable','on','UpdateFcn',#customUpdateFunction)
2) Custom update function evaluating the chosen datatip and creating an impoint
function txt = customUpdateFunction(empt,event_obj)
pos = get(event_obj,'Position');
ax = get(event_obj.Target,'parent');
sc = get(ax,'children');
x = sc.XData;
y = sc.YData;
mask = x == pos(1) & y == pos(2);
x(mask) = NaN;
y(mask) = NaN;
set(sc, 'XData', x, 'YData', y);
set(datacursormode(gcf),'Enable','off')
impoint(ax, pos(1),pos(2));
delete(findall(ax,'Type','hggroup','HandleVisibility','off'));
txt = {};
It works for the, if you'd just want to move one point. Reactivating the datacursormode and setting a second point fails:
Maybe you can find the error.
I have an user interactive animation in matlab where a shape is translated and rotates across a plot and the user has to click on it. If they click on it, their score is incremented and if not, the animation stops. For some reason the program does not seem to be registering the clicks from the user and I am unsure why. I have posted the code below. Any help would be greatly appreciated.
Animation Function:
function movingPolygon
global gUserHitPolygon;
global gCurrentXVertices;
global gCurrentYVertices;
gUserHitPolygon = true;
global gScore;
nSides =4;
%Polar points
r=1;
theta = pi/nSides * (1:2:2*nSides-1);
%Cartesisn points
x0 = r * cos(theta);
y0 = r * sin(theta);
nFrames = 100;
xx = linspace(0,10, nFrames);
yy = xx;
rr = linspace(0, 2*pi, nFrames);
h = figure;
set(h,'WindowButtonDownFcn', #mouseDownCallback);
i=1;
while gUserHitPolygon
rX = [cos(rr(i)), -sin(rr(i))];
rY = [sin(rr(i)), cos(rr(i))];
x1 = rX * [x0; y0];
y1 = rY * [x0; y0];
x2= x1 + xx(i);
y2= y1 + yy(i);
gCurrentXVertices=x2;
gCurrentYVertices=y2;
y=fill(x2, y2, 'b');
xlim([0,10]); ylim([0,10]);
hold on;
pause(0.000000003);
delete(y);
title(sprintf('Score: %d', gScore));
i=i+1;
if i>nFrames
i=1;
end
end
end
Callback Function
function mouseDownCallback(~,~)
global gUserHitPolygon;
global gCurrentXVertices;
global gCurrentYVertices;
global gScore;
gScore=0;
xVertices = gCurrentXVertices;
yVertices = gCurrentYVertices;
% if we have valid (so non-empty) sets of x- and y-vertices then...
if isempty(xVertices) && isempty(yVertices)
% get the coordinate on the current axis
coordinates = get(gca,'CurrentPoint');
coordinates = coordinates(1,1:2);
% if the coordinate is not in the polygon, then change the
% flag
if inpolygon(coordinates(1),coordinates(2),xVertices,yVertices)
gUserHitPolygon = false;
else
gScore=gScore+1;
end
end
end
I found 3 issues with your code.
First, you initialise the value of gScore as 0 inside the callback function. That means that every time the mouse is clicked, gScore is set to 0. Instead, initialise it to 0 in the main function, just after you define it as a global variable, and just call global gScore in the callback.
Secondly, you are using an if statement to determine whether you have a valid set of coordinates from the click, and if so, you want to execute some code. However, your if statement is testing whether the inputs are empty, and only if they are is it running. Change the if statement to
if ~isempty(xVertices) && ~isempty(yVertices)
which will evaluate to 1 if you have valid (non-empty) inputs.
Thirdly, your test for whether the user clicks inside or outside the polygon has the same problem. If the user clicks inside the polygon, inpolygon will be true, and so you will execute gUserHitPolygon = false;, rather than the intended gScore=gScore+1;. To fix this, again, negate your condition:
if ~inpolygon(coordinates(1),coordinates(2),xVertices,yVertices)
I'm trying to model projectile motion with drag in Matlab. Everything works perfectly....except I can't figure out how to get it to stop when the "bullet" hits the ground.
I initially tried an iteration loop, defining a data array, and emptying cells of that array for when the y value was negative....unfortunately the ode solver didn't like that too much.
Here is my code
function [ time , x_position , y_position ] = shell_flight_simulator(m,D,Ve,Cd,ElAng)
rho=1.2; % kg/m^3
g=9.84; % acceleration due to gravity
A = pi.*(D./2).^2; % m^2, shells cross-sectional area (area of circle)
function [lookfor,stop,direction] = linevent(t,y);
% stop projectile when it hits the ground
lookfor = y(1); %Sets this to 0
stop = 1; %Stop when event is located
direction = -1; %Specify downward direction
options = odeset('Events',#event_function); % allows me to stop integration at an event
function fvec = projectile_forces(x,y)
vx=y(2);
vy=y(4);
v=sqrt(vx^2+vy^2);
Fd=1/2 * rho * v^2 * Cd * A;
fvec(1) = y(2);
fvec(2) = -Fd*vx/v/m;
fvec(3) = y(4);
fvec(4) = -g -Fd*vy/v/m;
fvec=fvec.';
end
tspan=[0, 90]; % time interval of interest
y0(1)=0; % initial x position
y0(2)=Ve*cos(ElAng); % vx
y0(3)=0; % initial y position
y0(4)=Ve*sin(ElAng); % vy
% using matlab solver
[t,ysol] = ode45(#projectile_forces, tspan, y0);
end
end
x = ysol(:,1);
vx = ysol(:,2);
y = ysol(:,3);
vy = ysol(:,4);
plot(x,y, 'r-');
xlabel('X Position (m)');
ylabel('Y Position (m)');
title ('Position Over Time');
end
I thought this would define an event when y=0 and stop the projectile, but it doesn't do anything. What am I doing wrong?
When trying to find the time at which the solution to the ODE reaches a certain level you should use an
Events function - see the BALLODE demo for an example that stops the solution process when one of the components of the solution reaches 0.