by Meghna Sharma
The pool starts out with the classic, “take your marks, go!” signage through white flashing lights. The swimmer begins to swim fast in the pool, on fire and at the start of their race, while the lights are racing in green. The swimmer then picks up a more gradual pace as the lights turn to blue flashing. In the third leg of the race, the swimmer’s stroke starts limping, the timing becomes uneven, and the lights are flashing white, symbolizing raising a white flag. Eventually, the swimmer stops and the lights become red, signifying failure in the race or the end of the journey in a sport.
In combination with my other class ME130, Designing for Planar Machinery, I synthesized and analyzed this linkage to have the motion, speed, and look of an arm during the freestyle stroke, hence actuated in this project to represent a swimmer.
As a lifelong swimmer since age 4, the sport became a part of my identity. Especially leading up to covid, I was at the peak of my swimming career and felt on fire. However, as with many athletes, you leave high school and no longer compete, often coined as childhood sports burnout. The loss of identity as an athlete and activity of my lifelong sport sparked many feelings of tiredness and even laziness. This project, Just Keep Swimming, embodies my journey of swimming through the duration of one race.
I knew I wanted to combine this final project with the motion synthesis basic tasks in my ME130 class and knew I wanted it to be ‘biomimicry’, or reflective of natural legged motion. I considered an animal and leaving tracks or prints, but wanted to choose something more personal to me that I could express in this final project.
This led me to creating a swimmer, as the human body is an innate linkage system.
To synthesize how to make the swimming motion via a 4-bar linkage (the scope of ME130), I thought of doing analysis to match the upper body’s path of motion. Due to time and lack of online resources on this topic, I synthesized just 3 key points of the stroke, while optimizing for desired arm length, orientation, and timing.
Finding the motion to replicate:
I created two vector loops between the (1) first and second position and (2) the first and third position. Each vector loop has a real and imaginary component. Each vector loop also has a left and right hand side. This leaves 8 equations, and linearly, 8 variables to solve through a matrix. The MATLAB code for how I synthesized that can be found here:
% ---------- FULL REVISED CODE: WZ + US SYSTEMS ----------
% Given known vectors
W1x = 26.25;
W1y = 14.53;
Z1x = 33.75;
Z1y = 4.53;
% Fixed knowns
p21 = 91.97;
p31 = 110.11;
delta2 = deg2rad(209.36);
delta3 = deg2rad(182.6);
% Matrix solve for angles
syms beta2 beta3 alpha2 alpha3 real
A = cos(beta2) - 1;
B = sin(beta2);
C = cos(alpha2) - 1;
D = sin(alpha2);
F = cos(beta3) - 1;
G = sin(beta3);
H = cos(alpha3) - 1;
K = sin(alpha3);
E = W1x*A - W1y*B + Z1x*C - Z1y*D;
L = W1x*F - W1y*G + Z1x*H - Z1y*K;
M = W1x*B + W1y*A + Z1x*D + Z1y*C;
N = W1x*G + W1y*F + Z1x*K + Z1y*H;
eqns = [E == p21*cos(delta2);
L == p31*cos(delta3);
M == p21*sin(delta2);
N == p31*sin(delta3)];
sol = vpasolve(eqns, [beta2, beta3, alpha2, alpha3]);
beta2_deg = rad2deg(double(sol.beta2));
beta3_deg = rad2deg(double(sol.beta3));
alpha2_deg = rad2deg(double(sol.alpha2));
alpha3_deg = rad2deg(double(sol.alpha3));
alpha2 = deg2rad(alpha2_deg);
alpha3 = deg2rad(alpha3_deg);
gamma2 = deg2rad(90);
gamma3 = deg2rad(260);
% Build coefficient matrix to solve for U1 and S1
A = cos(gamma2) - 1;
B = sin(gamma2);
C = cos(alpha2) - 1;
D = sin(alpha2);
F = cos(gamma3) - 1;
G = sin(gamma3);
H = cos(alpha3) - 1;
K = sin(alpha3);
M1 = [A, -B, C, -D;
F, -G, H, -K;
B, A, D, C;
G, F, K, H];
M3 = [p21*cos(delta2);
p31*cos(delta3);
p21*sin(delta2);
p31*sin(delta3)];
M2 = M1 \\ M3;
U1x = M2(1); U1y = M2(2);
S1x = M2(3); S1y = M2(4);
U1 = [U1x, U1y];
S1 = [S1x, S1y];
s = norm(S1);
u = norm(U1);
sigma = atan2(U1y, U1x);
psi = atan2(S1y, S1x);
% Construct rotated U2/U3 and S2/S3 using complex math
U1c = U1x + 1i*U1y;
S1c = S1x + 1i*S1y;
U2c = U1c * exp(1i * gamma2);
U3c = U1c * exp(1i * gamma3);
S2c = S1c * exp(1i * alpha2);
S3c = S1c * exp(1i * alpha3);
U2 = [real(U2c), imag(U2c)];
U3 = [real(U3c), imag(U3c)];
S2 = [real(S2c), imag(S2c)];
S3 = [real(S3c), imag(S3c)];
% ---------- GRAPHING ----------
figure; hold on; axis equal; grid on;
title('Full Vector Loop Diagram (3 Legs)');
xlabel('X'); ylabel('Y');
W1 = [W1x, W1y];
Z1 = [Z1x, Z1y];
P1 = W1 + Z1;
P21 = p21 * [cos(delta2), sin(delta2)];
P31 = p31 * [cos(delta3), sin(delta3)];
P2 = P1 + P21;
P3 = P1 + P31;
W1_angle = atan2(W1(2), W1(1));
Z1_angle = atan2(Z1(2), Z1(1));
W1_mag = norm(W1); Z1_mag = norm(Z1);
beta2 = deg2rad(beta2_deg);
beta3 = deg2rad(beta3_deg);
W2 = W1_mag * [cos(W1_angle + beta2), sin(W1_angle + beta2)];
Z2 = Z1_mag * [cos(Z1_angle + alpha2), sin(Z1_angle + alpha2)];
Z2_tail = P2 - Z2;
W3 = W1_mag * [cos(W1_angle + beta3), sin(W1_angle + beta3)];
Z3 = Z1_mag * [cos(Z1_angle + alpha3), sin(Z1_angle + alpha3)];
Z3_tail = P3 - Z3;
% Draw W/Z system
quiver(0, 0, W1(1), W1(2), 0, 'b', 'LineWidth', 2, 'DisplayName', 'W1');
quiver(W1(1), W1(2), Z1(1), Z1(2), 0, 'b', 'LineWidth', 2, 'DisplayName', 'Z1');
quiver(0, 0, W2(1), W2(2), 0, 'r', 'LineWidth', 2, 'DisplayName', 'W2');
quiver(Z2_tail(1), Z2_tail(2), Z2(1), Z2(2), 0, 'r', 'LineWidth', 2, 'DisplayName', 'Z2');
quiver(0, 0, W3(1), W3(2), 0, 'g', 'LineWidth', 2, 'DisplayName', 'W3');
quiver(Z3_tail(1), Z3_tail(2), Z3(1), Z3(2), 0, 'g', 'LineWidth', 2, 'DisplayName', 'Z3');
quiver(P1(1), P1(2), P21(1), P21(2), 0, 'k', 'LineWidth', 2, 'DisplayName', 'P21');
quiver(P1(1), P1(2), P31(1), P31(2), 0, 'k', 'LineWidth', 2, 'DisplayName', 'P31');
% U+S vector side
S1_tail = P1 - S1;
U_fixed = S1_tail - U1;
quiver(S1_tail(1), S1_tail(2), S1(1), S1(2), 0, 'Color', [1 0 1], 'LineWidth', 2, 'DisplayName', 'S1');
quiver(U_fixed(1), U_fixed(2), U1(1), U1(2), 0, 'Color', [1 0 1], 'LineWidth', 2, 'DisplayName', 'U1');
S2_tail = P2 - S2;
S3_tail = P3 - S3;
quiver(S2_tail(1), S2_tail(2), S2(1), S2(2), 0, 'Color', [1 0.5 0], 'LineWidth', 2, 'DisplayName', 'S2');
quiver(U_fixed(1), U_fixed(2), U2(1), U2(2), 0, 'Color', [1 0.5 0], 'LineWidth', 2, 'DisplayName', 'U2');
quiver(S3_tail(1), S3_tail(2), S3(1), S3(2), 0, 'Color', [0.5 0 0.8], 'LineWidth', 2, 'DisplayName', 'S3');
quiver(U_fixed(1), U_fixed(2), U3(1), U3(2), 0, 'Color', [0.5 0 0.8], 'LineWidth', 2, 'DisplayName', 'U3');
% Closure errors
error_US2 = norm((U2 - U1) + (S2 - S1) - P21);
error_US3 = norm((U3 - U1) + (S3 - S1) - P31);
fprintf('\\n--- Closure Errors ---\\n');
fprintf('U2+S2 - U1-S1 - P21 = %.6f\\n', error_US2);
fprintf('U3+S3 - U1-S1 - P31 = %.6f\\n', error_US3);
% Additional vector info and closure validation
vec_info = @(v) struct('Mag', norm(v), 'Angle', rad2deg(atan2(v(2), v(1))));
U1_info = vec_info(U1);
S1_info = vec_info(S1);
U2_info = vec_info(U2);
S2_info = vec_info(S2);
P21_info = vec_info(P21);
fprintf('\\n--- U + S VECTOR INFO ---\\n');
fprintf('U1: Mag = %.4f, Angle = %.2f\\xB0\\n', U1_info.Mag, U1_info.Angle);
fprintf('S1: Mag = %.4f, Angle = %.2f\\xB0\\n', S1_info.Mag, S1_info.Angle);
fprintf('U2: Mag = %.4f, Angle = %.2f\\xB0\\n', U2_info.Mag, U2_info.Angle);
fprintf('S2: Mag = %.4f, Angle = %.2f\\xB0\\n', S2_info.Mag, S2_info.Angle);
fprintf('P21: Mag = %.4f, Angle = %.2f\\xB0\\n', P21_info.Mag, P21_info.Angle);
deltaU = U2 - U1;
deltaS = S2 - S1;
sumDelta = deltaU + deltaS;
sumDelta_info = vec_info(sumDelta);
fprintf('\\n--- VECTOR SUM CHECK (U+S side) ---\\n');
fprintf('U2 - U1: [%.4f, %.4f]\\n', deltaU(1), deltaU(2));
fprintf('S2 - S1: [%.4f, %.4f]\\n', deltaS(1), deltaS(2));
fprintf('Sum (\\x394U+\\x394S): [%.4f, %.4f], Mag = %.4f, Angle = %.2f\\xB0\\n', sumDelta(1), sumDelta(2), sumDelta_info.Mag, sumDelta_info.Angle);
p21_error_us = norm(sumDelta - P21);
fprintf('Closure error |\\x394U + \\x394S - P21| = %.6f\\n', p21_error_us);
W1_info = vec_info(W1);
Z1_info = vec_info(Z1);
W2_info = vec_info(W2);
Z2_info = vec_info(Z2);
P21_info = vec_info(P21);
fprintf('\\n--- VECTOR INFO ---\\n');
fprintf('W1: Mag = %.4f, Angle = %.2f\\xB0\\n', W1_info.Mag, W1_info.Angle);
fprintf('Z1: Mag = %.4f, Angle = %.2f\\xB0\\n', Z1_info.Mag, Z1_info.Angle);
fprintf('W2: Mag = %.4f, Angle = %.2f\\xB0\\n', W2_info.Mag, W2_info.Angle);
fprintf('Z2: Mag = %.4f, Angle = %.2f\\xB0\\n', Z2_info.Mag, Z2_info.Angle);
fprintf('P21: Mag = %.4f, Angle = %.2f\\xB0\\n', P21_info.Mag, P21_info.Angle);
deltaW = W2 - W1;
deltaZ = Z2 - Z1;
sumDelta = deltaW + deltaZ;
sumDelta_info = vec_info(sumDelta);
fprintf('\\n--- VECTOR SUM CHECK ---\\n');
fprintf('W2 - W1: [%.4f, %.4f]\\n', deltaW(1), deltaW(2));
fprintf('Z2 - Z1: [%.4f, %.4f]\\n', deltaZ(1), deltaZ(2));
fprintf('Sum (\\x394W+\\x394Z): [%.4f, %.4f], Mag = %.4f, Angle = %.2f\\xB0\\n', sumDelta(1), sumDelta(2), sumDelta_info.Mag, sumDelta_info.Angle);
p21_error = norm(sumDelta - P21);
fprintf('Closure error |\\x394W + \\x394Z - P21| = %.6f\\n', p21_error);
legend('show', 'Location', 'bestoutside');
fprintf('W Mag = %.4f\\n', W1_info.Mag);
fprintf('Z Mag = %.4f\\n', Z1_info.Mag);
fprintf('S Mag = %.4f\\n', S1_info.Mag);
fprintf('U Mag = %.4f\\n', U1_info.Mag);
U1_tip = U_fixed + U1;
W1_tip = W1;
distance_tips = norm(U1_tip - W1_tip);
fprintf('AB Distance: %.4f\\n', distance_tips);
fprintf('O4: [%.4f, %.4f]\\n', U_fixed(1), U_fixed(2));
The printout here solves for the remaining variables while I was able to input the starting position link lengths and orientation (magnitude and direction of W, Z), the 3 key points (P1, P2, P3), and the timing (Gamma2, Gamma3). Here is a visual below of the labeled variables and the MATLAB output:
I went through a lot of iterations of my free choices, gamma2 and gamma3, to prevent the system from being susceptible to buckling. I iterated through combinations of 𝛄2 and 𝛄3 and I found that 𝛄2=90° and 90°<𝛄3<360° solved this problem. To further optimize, 𝛄3 at 260° gave the smallest coupler AB line, so I chose this to make it look more like a realistic elbow/arm. This gave me my final linkage which I analyzed via MATLAB animation:
clc; clear; close all;
% === FIXED POINTS ===
O2 = [0, 0];
O4 = [14.4, -2.1];
% === LINK LENGTHS ===
L_W = 30;
L_U = 18.6;
L_S_base = 30.4;
L_S_left = 34;
L_S_right = 48.4;
% === INITIAL CONFIGURATION ===
W_initial_tip = [26.25, 14.53];
theta_offset = atan2(W_initial_tip(2), W_initial_tip(1));
n_frames = 200;
theta_w = linspace(0, 2*pi, n_frames) + theta_offset; % One full revolution
C_path = nan(n_frames, 2); % To store coupler top vertex trail
% === BACKGROUND PATH ===
bg_points = [...
35, 17;
39, 7;
40, -5; % PULL
25, -13;
0, -24;
-20, -30; % PUSH
-30, -23;
-40, -15;
-50, -8; % RECOVERY
-25, 0;
0, -9;
25, 15;
55, 10 % ENTRY
];
bg_labels = {
'', '', 'PULL', '', '', ...
'PUSH', '', '', 'RECOVERY', '', '', '', 'ENTRY'
};
% === SETUP FIGURE ===
figure;
axis equal;
axis([-60 70 -60 50]);
hold on;
% Smooth and plot background curve
t = 1:size(bg_points,1);
tsmooth = linspace(1, length(t), 300);
x_smooth = interp1(t, bg_points(:,1), tsmooth, 'spline');
y_smooth = interp1(t, bg_points(:,2), tsmooth, 'spline');
plot(x_smooth, y_smooth, 'Color', [0.5 0.5 0.5], 'LineWidth', 1.8); % smooth gray path
scatter(bg_points(:,1), bg_points(:,2), 40, 'k', 'filled', 'MarkerFaceAlpha', 0.2);
for i = 1:length(bg_labels)
if ~isempty(bg_labels{i})
text(bg_points(i,1)+1.5, bg_points(i,2), bg_labels{i}, ...
'Color', 'k', 'FontWeight', 'bold', 'FontSize', 9);
end
end
% === INITIALIZE GRAPHICS OBJECTS ===
h_crankW = plot(NaN, NaN, 'r-', 'LineWidth', 2); % Crank W
h_outputU = plot(NaN, NaN, 'g-', 'LineWidth', 2); % Crank U
h_coupler = fill(NaN, NaN, [0.7 0.9 1], 'EdgeColor', 'b', 'LineWidth', 1.5); % Coupler Triangle
h_trail = plot(NaN, NaN, 'r.', 'MarkerSize', 6); % Trail of top vertex
plot(O2(1), O2(2), 'ko', 'MarkerFaceColor', 'k'); % O2 pivot
plot(O4(1), O4(2), 'ko', 'MarkerFaceColor', 'k'); % O4 pivot
% === ANIMATION LOOP ===
for k = 1:n_frames
% Step 1: Crank W rotation (clockwise, hence -theta)
A = O2 + L_W * [cos(-theta_w(k)), sin(-theta_w(k))];
% Step 2: Solve for U_tip using circle intersection
C1 = A; R1 = L_S_base;
C2 = O4; R2 = L_U;
D = norm(C2 - C1);
if D > R1 + R2 || D < abs(R1 - R2)
continue;
end
a = (R1^2 - R2^2 + D^2) / (2*D);
h = sqrt(R1^2 - a^2);
P2 = C1 + a * (C2 - C1)/D;
perp = [- (C2(2) - C1(2)), C2(1) - C1(1)] / D;
B = P2 + h * perp;
% Step 3: Compute top vertex of triangle coupler
AB = B - A; d = norm(AB);
c = L_S_right; b = L_S_left;
if d == 0
continue;
end
x = (c^2 - b^2 + d^2) / (2*d);
y = sqrt(c^2 - x^2);
unit = AB / d;
normal = [-unit(2), unit(1)];
C = A + x * unit + y * normal;
% Step 4: Store trail
C_path(k, :) = C;
% Step 5: Update graphics
set(h_crankW, 'XData', [O2(1), A(1)], 'YData', [O2(2), A(2)]);
set(h_outputU, 'XData', [O4(1), B(1)], 'YData', [O4(2), B(2)]);
set(h_coupler, 'XData', [A(1), B(1), C(1)], 'YData', [A(2), B(2), C(2)]);
set(h_trail, 'XData', C_path(1:k,1), 'YData', C_path(1:k,2));
title(sprintf('4-Bar Linkage Animation | Frame %d of %d', k, n_frames));
drawnow;
end