You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
284 lines
9.5 KiB
284 lines
9.5 KiB
|
|
%%
|
|
% Today I will show you how I plotted this treble clef in MATLAB:
|
|
%
|
|
% <<https://blogs.mathworks.com/steve/files/treble-clef-screen-shot.png>>
|
|
%
|
|
% My discovery and implementation process for doing this involved Unicode
|
|
% characters, SVG files, string processing, Bezier curves, |kron|, implicit
|
|
% expansion, and |polyshape|. I thought it was fun, and I learned a few
|
|
% things, so I wanted to share it.
|
|
%
|
|
% As a personal project, I make specialized fingering charts for the French
|
|
% horn. I do this in MATLAB, of course. For one of these charts, I wanted
|
|
% to be able to place several treble clef symbols, precisely and with high
|
|
% quality. That's I got started on this quest.
|
|
%
|
|
% First, I wondered if there is a Unicode symbol for treble clef (also
|
|
% known as the G clef). There is. One online resource I like to use for
|
|
% Unicode is <https://www.fileformat.info/info/unicode/index.htm
|
|
% FileFormat.info>, which has this info page for the treble clef:
|
|
%
|
|
% <<https://blogs.mathworks.com/steve/files/fileformat-info-screen-shot.png>>
|
|
%
|
|
% But I wasn't sure that using the Unicode character directly was really
|
|
% going to work out for me. The precise positioning and size of a Unicode
|
|
% character is too dependent on which font one uses. But the line
|
|
% <https://www.fileformat.info/info/unicode/char/1d11e/musical_symbol_g_clef.svg
|
|
% "Outline (as SVG file)"> on that info page caught my eye. Maybe I could
|
|
% use that?
|
|
%
|
|
% I don't know that much about SVG files, but I downloaded the file and
|
|
% looked at it in a text editor. There didn't seem to be much to it:
|
|
%
|
|
% <<https://blogs.mathworks.com/steve/files/clef-svg-file-screen-shot.png>>
|
|
%
|
|
% Clearly, I needed to understand more about an SVG _path_. I found this
|
|
% <https://www.w3.org/TR/SVG/paths.html w3.org> page that explains SVG
|
|
% paths in detail. Fortunately for me, since I just wanted to try a quick
|
|
% prototype, the path in the treble clef outline file contained only a few
|
|
% commands:
|
|
%
|
|
% * "M" - move to a position
|
|
% * "L" - draw a line to a position
|
|
% * "Q" - draw a quadratic Bezier curve segment using three points
|
|
% * "Z" - close the current curve
|
|
%
|
|
% I certainly didn't want to write a fully general SVG path parser just to
|
|
% do a prototype. Instead, I used some MATLAB string processing
|
|
% functionality to break the path string down into easily processable
|
|
% chunks. Here's the string I was dealing with.
|
|
cubic=1;
|
|
load wrench_svg
|
|
clef_path=wrench{1};
|
|
% https://www.fileformat.info/info/unicode/char/1d11e/musical_symbol_g_clef.svg
|
|
|
|
%%
|
|
% I started with putting each command on a separate line.
|
|
commands = 'CMQLZ';
|
|
for k = 1:length(commands)
|
|
ck = string(commands(k));
|
|
clef_path = replace(clef_path,ck,newline + ck);
|
|
end
|
|
|
|
%%
|
|
% And then I split up the string by line breaks to make a string array.
|
|
|
|
clef_path = splitlines(clef_path);
|
|
% clef_path(1:15)
|
|
|
|
%%
|
|
% Now I need to explain about those "Q" commands. They indicate the drawing
|
|
% of a quadratic Bezier curve using three control points (the current point
|
|
% and the two additional points listed following the "Q" character). I
|
|
% wasn't familiar with the mathematics of drawing Bezier curves, but I
|
|
% quickly found <https://blogs.mathworks.com/graphics/2014/10/13/bezier-curves/
|
|
% Mike Garrity's old graphics blog post> on the subject. Here's a brief
|
|
% snippet of explanation and code from that old post.
|
|
|
|
P1 = [ 5; -10];
|
|
P2 = [18; 18];
|
|
P3 = [45; 15];
|
|
|
|
cla
|
|
placelabel(P1,'P_1');
|
|
placelabel(P2,'P_2');
|
|
placelabel(P3,'P_3');
|
|
xlim([0 50])
|
|
axis equal
|
|
|
|
%%
|
|
% _[Mike's explanation]_ And we've still got $t$ going from 0 to 1, but
|
|
% we'll use second-order polynomials like this:
|
|
%
|
|
% $$P(t) = (1-t)^2 P_1 + 2 (1-t) t P_2 + t^2 P_3$$
|
|
%
|
|
% These are known as the <http://en.wikipedia.org/wiki/Bernstein_polynomial
|
|
% Bernstein basis polynomials>.
|
|
%
|
|
% We can use the kron function to evaluate them.
|
|
t = linspace(0,1,101);
|
|
P = kron((1-t).^2,P1) + kron(2*(1-t).*t,P2) + kron(t.^2,P3);
|
|
|
|
hold on
|
|
plot(P(1,:),P(2,:))
|
|
hold off
|
|
|
|
%%
|
|
% Notice that the resulting curve starts at $P_1$ and ends at $P_3$. In
|
|
% between, it moves towards, but doesn't reach, $P_2$. _[End of Mike's
|
|
% explanation]_
|
|
%
|
|
% The |kron| function, used by Mike, computes something called the
|
|
% Kronecker tensor product. However, since implicit expansion was
|
|
% introduced in MATLAB three years ago, this is more easily computed simply
|
|
% by using element-wise multiplication.
|
|
|
|
t = t;
|
|
P = ((1-t).^2 .* P1) + (2*(1-t) .* t .* P2) + (t.^2 .* P3);
|
|
|
|
%%
|
|
% So, that's the computation I need to perform for each "Q" command in the
|
|
% SVG path. I don't think I need 101 points to draw each Bezier curve
|
|
% segment, though; let's go with 21. Also, I'm going to change the
|
|
% orientation to be a column.
|
|
|
|
t = linspace(0,1,21)';
|
|
|
|
%%
|
|
% Now let's build up |x| and |y| vectors based on each SVG path command.
|
|
if cubic
|
|
l=4;
|
|
elseif quadratic
|
|
l=3;
|
|
else
|
|
l=1;
|
|
end
|
|
x = zeros(0,l);
|
|
y = zeros(0,l);
|
|
|
|
x_current = 0;
|
|
y_current = 0;
|
|
curves={};
|
|
ccounter=1;
|
|
cccounter=1;
|
|
currentcurve=zeros(0,1);
|
|
xxcounter=1;
|
|
for k = 1:length(clef_path)
|
|
if clef_path(k) == ""
|
|
continue
|
|
end
|
|
|
|
% Get the command character and a vector of the numeric values after
|
|
% the command.
|
|
command = extractBefore(clef_path(k),2);
|
|
remainder = extractAfter(clef_path(k),1);
|
|
remainder = strrep(remainder, ',', ' ')
|
|
values = str2double(split(remainder));
|
|
if isnan(values(1))
|
|
values=values(2:(end-1));
|
|
end
|
|
|
|
switch command
|
|
case "M"
|
|
% Move to a position. Put a NaN-valued point in the vector.
|
|
curves{ccounter}=currentcurve;
|
|
currentcurve=zeros(0,l);
|
|
cccounter=1;
|
|
ccounter=ccounter+1;
|
|
|
|
x = [x ; NaN];
|
|
y = [y ; NaN];
|
|
x_current = values(1);
|
|
y_current = values(2);
|
|
x(end+1,1) = x_current;
|
|
y(end+1,1) = y_current;
|
|
% curve{k}=[x;y];
|
|
case "L"
|
|
% Draw a line segment from the current point to the specified
|
|
% point.
|
|
if l==3
|
|
currentcurve(cccounter:(cccounter+2),:)=[[x_current (x_current+values(1))/2 values(1)]; [y_current (y_current+values(2))/2 values(2)]; [1 1 1]];
|
|
elseif l==4
|
|
currentcurve(cccounter:(cccounter+2),:)=[[x_current (2/3*x_current+1/3*values(1)) (1/3*x_current+2/3*values(1)) values(1)]; [y_current (2/3*y_current+1/3*values(2)) (1/3*y_current+2/3*values(2)) values(2)]; [1 1 1 1]];
|
|
end
|
|
cccounter=cccounter+3;
|
|
x_current = values(1);
|
|
y_current = values(2);
|
|
x(end+1,1) = x_current;
|
|
y(end+1,1) = y_current;
|
|
% curve{k}=[values x;y];
|
|
case "Q"
|
|
% Draw a quadratic Bezier curve segment using the current point
|
|
% and the two additional points as control points.
|
|
|
|
pt1 = [x_current y_current];
|
|
pt2 = [values(1) values(2)];
|
|
pt3 = [values(3) values(4)];
|
|
currentcurve(cccounter:(cccounter+2),:)=[[x_current values(1) values(3)]; [y_current values(2) values(4)]; [1 1 1]];
|
|
cccounter=cccounter+3;
|
|
pts = ((1-t).^2 .* pt1) + (2*(1-t).*t .* pt2) + ...
|
|
(t.^2 * pt3);
|
|
x = [x ; pts(:,1)];
|
|
y = [y ; pts(:,2)];
|
|
|
|
x_current = values(3);
|
|
y_current = values(4);
|
|
case "C"
|
|
% Draw a quadratic Bezier curve segment using the current point
|
|
% and the two additional points as control points.
|
|
|
|
pt1 = [x_current y_current];
|
|
pt2 = [values(1) values(2)];
|
|
pt3 = [values(3) values(4)];
|
|
pt4 = [values(5) values(6)];
|
|
currentcurve(cccounter:(cccounter+2),:)=[[x_current values(1) values(3) values(5)]; [y_current values(2) values(4) values(6)]; [1 1 1 1]];
|
|
cccounter=cccounter+3;
|
|
pts = ((1-t).^3 .* pt1) + (3*(1-t).^2.*t .* pt2) + ...
|
|
(3*(1-t).*t.^2 * pt3) + ((t.^3.*pt4));
|
|
x = [x ; pts(:,1)];
|
|
y = [y ; pts(:,2)];
|
|
|
|
x_current = values(5);
|
|
y_current = values(6);
|
|
case "Z"
|
|
% TODO: curve-closing logic
|
|
curves{ccounter}=currentcurve;
|
|
end
|
|
plot(x,y);
|
|
% close
|
|
end
|
|
curves{ccounter}=currentcurve;
|
|
%%
|
|
% Now, let's take a big breath and see what we've got!
|
|
|
|
plot(x,y)
|
|
|
|
%%
|
|
% *So close!* But upside down and oddly squished. Both those problems are
|
|
% easily fixed.
|
|
|
|
axis equal
|
|
axis ij
|
|
|
|
%%
|
|
% *Even closer!* But how do we get a filled shape?? That's where
|
|
% |polyshape| comes in. We've got |x| and |y| vectors containing four
|
|
% segments separated by |NaN| values. The four segments are for the outer
|
|
% curve, plus the outlines of the three holes or voids in the treble clef.
|
|
% The constructor for |polyshape| knows exactly how to deal with that kind
|
|
% of data. It can figure out automatically which curves are outer
|
|
% boundaries and which curves are inner boundaries. The resulting
|
|
% |polyshape| object can be easily plotted.
|
|
%
|
|
% *Note*: the duplicate points warning below can be safely ignored. If I
|
|
% were doing something other than a quick prototype, I would construct the
|
|
% data more carefully to avoid it.
|
|
%
|
|
% There you have it. You can plot a treble clef in MATLAB.
|
|
%
|
|
% What will you do with your new-found power?
|
|
|
|
p = polyshape(x,y);
|
|
plot(p)
|
|
axis equal
|
|
axis ij
|
|
|
|
%%
|
|
function placelabel(pt,str)
|
|
% Utility function for code demonstrating Bezier curve plotting
|
|
x = pt(1);
|
|
y = pt(2);
|
|
h = line(x,y);
|
|
h.Marker = '.';
|
|
h = text(x,y,str);
|
|
h.HorizontalAlignment = 'center';
|
|
h.VerticalAlignment = 'bottom';
|
|
end
|
|
|
|
%%
|
|
% _Copyright 2020 The MathWorks, Inc._
|
|
|
|
% for i=1:length(clef_path)
|
|
%
|
|
% end
|