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)
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 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 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
Goal: To perform several clicks in one figure, containing an image displayed with imshow and save the coordinates of the "clicked" point(s), to be used in further operations.
Notes: I know about the functions getpts/ginput but I would like to perform this without using them. Is this possible using ButtonDownFcn? (see the following code)
function testClicks
img = ones(300); % image to display
h = imshow(img,'Parent',gca);
set(h,'ButtonDownFcn',{#ax_bdfcn});
function ax_bdfcn(varargin)
a = get(gca,'CurrentPoint');
x = a(1,1);
y = a(1,2);
At this stage the variables x and y only "live" inside ax_bdfcn.
How can I make them available in the testClicks function? Is this possible using ButtonDownFcn? Is this a good approach?
Thanks a lot.
EDIT1:
Thanks for the answer Shai. But I still cannot accomplish what I intended.
function [xArray, yArray] = testClicks()
img = ones(300); % image to display
h = imshow(img,'Parent',gca);
x = [];
y = [];
xArray = [];
yArray = [];
stop = 0;
while stop == 0;
set(h,'ButtonDownFcn',{#ax_bdfcn});
xArray = [xArray x];
yArray = [yArray y];
if length(xArray)>15
stop = 1;
end
end
function ax_bdfcn(varargin)
a = get(gca, 'CurrentPoint');
assignin('caller', 'x', a(1,1) );
assignin('caller', 'y', a(1,2) );
end
end % must have end for nested functions
This code (buggy!) is the closest I can get to what I want (after all the clicking, having an array with the x and y coordinates of the clicked points). I am clealy not understanding the mechanics for the implementation of this task. Any help?
There are several ways
Using nested functions
function testClicks
img = ones(300); % image to display
h = imshow(img,'Parent',gca);
set(h,'ButtonDownFcn',{#ax_bdfcn});
x = []; % define "scope" of x and y
y = [];
% call back as nested function
function ax_bdfcn(varargin)
a = get(gca,'CurrentPoint');
x = a(1,1); % set x and y at caller scope due to "nested"ness of function
y = a(1,2);
end % close nested function
end % must have end for nested functions
Using assignin
function ax_bdfcn(varargin)
a = get(gca, 'CurrentPoint');
assignin('caller', 'x', a(1) );
assignin('caller', 'y', a(2) );
Using 'UserData' property of figure handle
function ax_bdfcn(varargin)
a = get(gca, 'CurrentPoint');
set( gcf, 'UserData', a(1:2) );
'UserData' can be accessed (as long as the figure is alive) using cp = get( gcf, 'UserData');.
EDIT:
An example of a way to "communicate" the clicked locations to 'base' workspace
function ax_bdfcn(varargin)
a = get(gca,'CurrentPoint');
% the hard part - assign points to base
if evalin('base', 'exist(''xArray'',''var'')')
xArray = evalin('base','xArray');
else
xArray = [];
end
xArray = [xArray a(1)]; % add the point
assignin('base','xArray',xArray); % save to base
% do the same for yArray
After calling testClicks there are NO xArray or yArray variables in the workspace (at least there shouldn't). After the first click these two variables will "miraculously" be created. After every other click these two arrays will increase their size until you close the figure.
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.