Mianzhi Wang

Ph.D. in Electrical Engineering

MATLAB Plots Can Be Beautiful III - Light Dots


After plotting trees and mountains, let us plot something bigger, something we can use as a wallpaper. In this tutorial, we will use MATLAB to draw glowing light dots. Below is a glimpse of the final product (you can click the image to view the full version).

Setting Everything Up

We start by writing down the boilerplate code and defining the necessary parameters we will use later. The description of each parameter is given in the comments. Note that when setting up the figure, we explicitly set the background to black and hide the axes. It is crucial to hide the axes and specify the plot area to be the whole window. Otherwise the axes and paddings will appear in the image to which the Gaussian filtering will be applied, leading to undesired artifacts.

Note: canvas_width and canvas_height must be smaller than your screen width and height, respectively. MATLAB will attempt to resize the figure window if it cannot fit into your screen, leading to incorrect plots.

function light_dots
%LIGHT_DOTS Draws fancy light dots.

% define canvas size
canvas_width = 1440;
canvas_height = 810;
% define the parameters controlling the curve that the light dots follows
sin_period = 1.2 * canvas_width;
init_phase = unifrnd(0, pi*2);
amp = 0.15 * canvas_height;
% define the parameters controlling the size and randomness of the light
% dots
base_radius = floor(0.09 * canvas_width);
x_randomness = 10;
y_randomness = 50;

% set up the figure
% we want the backgroud to be black
hf = figure('Menubar', 'none', 'Color', 'black', ...
    'Position', [0 0 canvas_width canvas_height], ...
    'Visible', 'on');
% we want to hide the axes so the actual plot will occupy the whole window
axis('equal');
set(gca, 'Position', [0 0 1 1]);
axis([0 canvas_width 0 canvas_height]);
axis('off');

end

Drawing and Blending

A unblurred light dot can be straightforwardly drawn with the rectangle function[1]. To make the light dots follow a curve, we just need to generate their centroid coordinates around a sine curve. The tricky part is getting the feeling of the depth (the blurry effect). This is done by alternating between drawing light dots and applying the Gaussian filtering several times. The whole procedure can be summarized in the following gif:

light_dots_procedure

When translated into the MATLAB code, the whole procedure becomes:

% draw and blend
draw_layer_and_blend(6, 0.3, 0.6, base_radius, 0.3, 1, 50);
draw_layer_and_blend(32, 0.6, 0.9, base_radius, 0.2, 12, 40);
draw_layer_and_blend(16, 0.65, 1.0, base_radius / 3, 0.1, 1, 2);
draw_layer_and_blend(16, 0.70, 1.0, base_radius / 4, 0.1, 1, 1);
draw_layer_and_blend(24, 0.75, 1.0, base_radius / 6, 0.2, 1, 1);
draw_random_dots(48, 0.95, 0.8, base_radius / 12, 0.3, 1);

Implementing draw_random_dots

The implementation of this function is quite straightforward. We first generate the centroid coordinates around a sine curve, as well as the radiuses, and the colors of each dot. We then plot then using the rectangle function. To achieve a smooth color transition, we use the x coordinates of the dots to control the hue values. The detailed implementation of draw_random_dots is given below:

function draw_random_dots(n, alpha, sat, r, r_rnd, ratio)
% Inputs:
%   n - Number of dots.
%   alpha - Transparency of the dots.
%   sat - Saturation of the dots.
%   r - Base radius of the dots.
%   r_rnd - Randomness in the radius.
%   ratio - Width of the dots = radius / ratio;

% generates the centriod coordiates
x_coords = linspace(0, canvas_width, n)' + x_randomness * randn(n, 1);
curve = amp * sin(init_phase + x_coords / sin_period * 2 * pi);
y_coords = curve + canvas_height / 2 + y_randomness * randn(n, 1);
% generate RGB colors according to the x coordinates
% here we use the x coordinate to control the hue value
colors = hsv2rgb([min(max(x_coords / canvas_width, 0), 1) sat*ones(n, 1) ones(n, 1)]);
% generate radiuses
radiuses = r * (1.0 + min(max(r_rnd * randn(n, 1), -1), 1));
% draw the light dots
for ii = 1:n
    w = radiuses(ii) / ratio;
    xywh = [x_coords(ii) - w y_coords(ii) - radiuses(ii) 2 * w 2 * radiuses(ii)];
    rectangle('Position', xywh, 'Curvature', [1 1], ...
        'FaceColor', [colors(ii,:) 0.8*alpha], ...
        'EdgeColor', [colors(ii,:) alpha], ...
        'LineWidth', 1);
end
end

Implementing draw_layer_and_blend

Here is where the magic happens. We first draw a group of light dots by calling draw_random_dots. We then retrieve the current plot as an image Io1\mathbf{I}_\mathrm{o}^1, and apply Gaussian filtering[2] to blur it. Next, we replace the current plot with the blurred image Ib1\mathbf{I}_\mathrm{b}^1. If we call this function again, a new group of light dots will be drawn over the blurred image Ib1\mathbf{I}_\mathrm{b}^1. The new plot will be converted to an new image Io2\mathbf{I}_\mathrm{o}^2, which will be blurred and become Ib2\mathbf{I}_\mathrm{b}^2. Hence, by calling draw_layer_and_blend repeatedly, we alternate between drawing light dots and applying the Gaussian filtering. The detailed implementation of draw_layer_and_blend is given below:

function draw_layer_and_blend(n, alpha, sat, r, r_rnd, ratio, sigma)
% Inputs:
%   n - Number of dots.
%   alpha - Transparency of the dots.
%   sat - Saturation of the dots.
%   r - Base radius of the dots.
%   r_rnd - Randomness in the radius.
%   ratio - Width of the dots = radius / ratio;
%   sigma - Standard deviation of the 2-D Gaussian smoothing kernel.

% draw the dots
draw_random_dots(n, alpha, sat, r, r_rnd, ratio);
% retrives the current plot as an image and apply the filter
frame = getframe(gca);
blurred_im = imgaussfilt(frame2im(frame), sigma);
im_size = size(blurred_im);
% replace the plot with the blurred image
clf(hf);
image(linspace(0, canvas_width, im_size(1)), ...
    linspace(0, canvas_height, im_size(2)), blurred_im);
% make sure the axes are hidden and the plot occupies the whole window
axis('equal');
axis([0 canvas_width 0 canvas_height]);
set(gca, 'Position', [0 0 1 1]);
axis('off');
end

Exporting

To export our final product as a high DPI image, we can simply use the print function[3]:

% save a high DPI version of our plot so you can use it as a wallpaper!
set(hf, 'PaperPositionMode', 'auto');
print(hf, 'light_dots.png', '-dpng', '-r300');

Putting Everything Together

Now we can put everything together and finalize our function:

function light_dots
%LIGHT_DOTS Draws fancy light dots.

% define canvas size
canvas_width = 1440;
canvas_height = 810;
% define the parameters controlling the curve that the light dots follows
sin_period = 1.2 * canvas_width;
init_phase = unifrnd(0, pi*2);
amp = 0.15 * canvas_height;
% define the parameters controlling the size and randomness of the light
% dots
base_radius = floor(0.09 * canvas_width);
x_randomness = 10;
y_randomness = 50;

% set up the figure
% we want the backgroud to be black
hf = figure('Menubar', 'none', 'Color', 'black', ...
    'Position', [0 0 canvas_width canvas_height], ...
    'Visible', 'on');
% we want to hide the axes so the actual plot will occupy the whole window
axis('equal');
set(gca, 'Position', [0 0 1 1]);
axis([0 canvas_width 0 canvas_height]);
axis('off');

% draw and blend
draw_layer_and_blend(6, 0.3, 0.6, base_radius, 0.3, 1, 50);
draw_layer_and_blend(32, 0.6, 0.9, base_radius, 0.2, 12, 40);
draw_layer_and_blend(16, 0.65, 1.0, base_radius / 3, 0.1, 1, 2);
draw_layer_and_blend(16, 0.70, 1.0, base_radius / 4, 0.1, 1, 1);
draw_layer_and_blend(24, 0.75, 1.0, base_radius / 6, 0.2, 1, 1);
draw_random_dots(48, 0.95, 0.8, base_radius / 12, 0.3, 1);

% save a high DPI version of our plot so you can use it as a wallpaper!
set(hf, 'PaperPositionMode', 'auto');
print(hf, 'light_dots.png', '-dpng', '-r300');

% nested functions
% ================

function draw_layer_and_blend(n, alpha, sat, r, r_rnd, ratio, sigma)
% Inputs:
%   n - Number of dots.
%   alpha - Transparency of the dots.
%   sat - Saturation of the dots.
%   r - Base radius of the dots.
%   r_rnd - Randomness in the radius.
%   ratio - Width of the dots = radius / ratio;
%   sigma - Standard deviation of the 2-D Gaussian smoothing kernel.

% draw the dots
draw_random_dots(n, alpha, sat, r, r_rnd, ratio);
% retrives the current plot as an image and apply the filter
frame = getframe(gca);
blurred_im = imgaussfilt(frame2im(frame), sigma);
im_size = size(blurred_im);
% replace the plot with the blurred image
clf(hf);
image(linspace(0, canvas_width, im_size(1)), ...
    linspace(0, canvas_height, im_size(2)), blurred_im);
% make sure the axes are hidden and the plot occupies the whole window
axis('equal');
axis([0 canvas_width 0 canvas_height]);
set(gca, 'Position', [0 0 1 1]);
axis('off');
end

function draw_random_dots(n, alpha, sat, r, r_rnd, ratio)
% Inputs:
%   n - Number of dots.
%   alpha - Transparency of the dots.
%   sat - Saturation of the dots.
%   r - Base radius of the dots.
%   r_rnd - Randomness in the radius.
%   ratio - Width of the dots = radius / ratio;

% generates the centriod coordiates
x_coords = linspace(0, canvas_width, n)' + x_randomness * randn(n, 1);
curve = amp * sin(init_phase + x_coords / sin_period * 2 * pi);
y_coords = curve + canvas_height / 2 + y_randomness * randn(n, 1);
% generate RGB colors according to the x coordinates
% here we use the x coordinate to control the hue value
colors = hsv2rgb([min(max(x_coords / canvas_width, 0), 1) sat*ones(n, 1) ones(n, 1)]);
% generate radiuses
radiuses = r * (1.0 + min(max(r_rnd * randn(n, 1), -1), 1));
% draw the light dots
for ii = 1:n
    w = radiuses(ii) / ratio;
    xywh = [x_coords(ii) - w y_coords(ii) - radiuses(ii) 2 * w 2 * radiuses(ii)];
    rectangle('Position', xywh, 'Curvature', [1 1], ...
        'FaceColor', [colors(ii,:) 0.8*alpha], ...
        'EdgeColor', [colors(ii,:) alpha], ...
        'LineWidth', 1);
end
end
end

  1. https://www.mathworks.com/help/matlab/ref/rectangle.html

  2. https://www.mathworks.com/help/images/ref/imgaussfilt.html

  3. https://www.mathworks.com/help/matlab/ref/print.html