# Exploring Randomness In JavaScript

In my post yesterday, on building a color palette utility in Alpine.js, randomness played a big part: each swatch was generated as a composite of randomly selected Hue `(0..360)`

, Saturation (`0..100`

), and Lightness (`0..100`

) values. As I was putting that demo together, I came across the Web Crypto API. Normally, when generating random values, I use the the `Math.random()`

method; but, the MDN docs mention that `Crypto.getRandomValues()`

is more secure. As such, I ended up trying `Crypto`

out (with a fallback to the `Math`

module as needed). But, this left me wondering if "more secure" actually meant "more random" for my particular use-case.

**Run this demo** in my **JavaScript Demos project** on GitHub.

**View this code** in my **JavaScript Demos project** on GitHub.

Randomness, from a security standpoint, has an actual meaning. I'm not a security expert; but, my understanding is that when a pseudo-random number generator (PRNG) is considered "secure", it means that the sequence of numbers that it will produce—or has already produced—cannot be deduces by an attacker.

When it comes to "random color generators", like my color palette utility, the notion of "randomness" is much more fuzzy. In my case, the random color generation is only as random as is "feels" to the user. In other words, the effectiveness of the randomness is part of the overall user experience (UX).

To this end, I want to try generating some random *visual* elements using both `Math.random()`

and `crypto.getRandomValues()`

to see if one of the methods *feels* substantively different. Each trial will contain a randomly generated `<canvas>`

element and a randomly generated set of integers. Then, I will use my (deeply flawed) human intuition to see if one of the trials looks "better" than the other.

The `Math.random()`

method works by returning a decimal value between `0`

(inclusive) and `1`

(exclusive). This can be used to generate random integers by taking the result of the randomness and multiplying it against a range of possible values.

In other words, if `Math.random()`

returned `0.25`

, you'd pick the value that lands closest to 25% along the given min-max range. And, if `Math.random()`

returned `0.97`

, you'd pick the value that lands closest to 97% along the given min-max range.

The `crypto.getRandomValues()`

method works very differently. Instead of returning a single value, you pass-in a Typed Array with a pre-allocated size (length). The `.getRandomValues()`

method then fills that array with random values dictated by the min/max values that can be stored by the given Type.

To make this exploration easier, I want both approaches to work roughly the same. So, instead of having to deal with decimals in one algorithm and integers in another algorithm, I'm going to force both algorithms to rely on decimal generation. Which means, I have to coerce the `value`

returned by `.getRandomValues()`

into a decimal (`0..1`

):

`value / ( maxValue + 1 )`

I'll encapsulate this difference in two method, `randFloatWithMath()`

and `randFloatWithCrypto()`

:

```
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Math module.
*/
function randFloatWithMath() {
return Math.random();
}
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Crypto module.
*/
function randFloatWithCrypto() {
var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
var maxInt = 4294967295;
return ( randomInt / ( maxInt + 1 ) );
}
```

With these two methods in place, I can then assign one of them to a `randFloat()`

reference which can be used to seamless generate random values along a given range using either algorithm interchangeably:

```
/**
* I generate a random integer in between the given min and max, inclusive.
*/
function randRange( min, max ) {
return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}
```

Now to create the experiment. The user interface is small and is powered by Alpine.js. Each trial uses the same Alpine.js component; but, its constructor receives an argument that determines which `randFloat()`

implementation will be used:

```
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<h1>
Exploring Randomness In JavaScript
</h1>
<div class="side-by-side">
<section x-data="Explore( 'math' )">
<h2>
Math Module
</h2>
<!-- A very large number of random {X,Y} coordinates. -->
<canvas
x-ref="canvas"
width="320"
height="320">
</canvas>
<!-- A very small number of random values coordinates. -->
<p x-ref="list"></p>
<p>
Duration: <span x-text="duration"></span>
</p>
</section>
<section x-data="Explore( 'crypto' )">
<h2>
Crypto Module
</h2>
<!-- A very large number of random {X,Y} coordinates. -->
<canvas
x-ref="canvas"
width="320"
height="320">
</canvas>
<!-- A very small number of random values coordinates. -->
<p x-ref="list"></p>
<p>
Duration: <span x-text="duration"></span>ms
</p>
</section>
</div>
<script type="text/javascript" src="./main.js" defer></script>
<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
</body>
</html>
```

As you can see, each `x-data="Explore"`

component contains two `x-refs`

: `canvas`

and `list`

. When the component is initialized, it will populate these two refs with random values using the methods, `fillCanvas()`

and `fillList()`

, respectively.

Here's my JavaScript / Alpine.js component:

```
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Math module.
*/
function randFloatWithMath() {
return Math.random();
}
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Crypto module.
*/
function randFloatWithCrypto() {
// This method works by filling the given array with random values of the given type.
// In our case, we only need one random value, so we're going to pass in an array of
// length 1.
// --
// Note: For better performance, we could cache the typed array and just keep passing
// the same reference in (cuts performance in half). But, we're exploring the
// randomness, not the performance.
var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
var maxInt = 4294967295;
// Unlike Math.random(), crypto is giving us an integer. To feed this back into the
// same kind of math equation, we have to convert the integer into decimal so that we
// can figure out where in our range our randomness leads us.
return ( randomInt / ( maxInt + 1 ) );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
function Explore( algorithm ) {
// Each instance of this Alpine.js component is assigned a different randomization
// strategy for floats (0..1). Other than that, the component instances behave exactly
// the same.
var randFloat = ( algorithm === "math" )
? randFloatWithMath
: randFloatWithCrypto
;
return {
duration: 0,
// Public methods.
init: init,
// Private methods.
fillCanvas: fillCanvas,
fillList: fillList,
randRange: randRange
}
// ---
// PUBLIC METHODS.
// ---
/**
* I initialize the Alpine.js component.
*/
function init() {
var startedAt = Date.now();
this.fillCanvas();
this.fillList();
this.duration = ( Date.now() - startedAt );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I populate the canvas with random {X,Y} pixels.
*/
function fillCanvas() {
var pixelCount = 200000;
var canvas = this.$refs.canvas;
var width = canvas.width;
var height = canvas.height;
var context = canvas.getContext( "2d" );
context.fillStyle = "deeppink";
for ( var i = 0 ; i < pixelCount ; i++ ) {
var x = this.randRange( 0, width );
var y = this.randRange( 0, height );
// As we add more pixels, let's make the pixel colors increasingly opaque. I
// was hoping that this might help show potential clustering of values.
context.globalAlpha = ( i / pixelCount );
context.fillRect( x, y, 1, 1 );
}
}
/**
* I populate the list with random 0-9 values.
*/
function fillList() {
var list = this.$refs.list;
var valueCount = 105;
var values = [];
for ( var i = 0 ; i < valueCount ; i++ ) {
values.push( this.randRange( 0, 9 ) );
}
list.textContent = values.join( " " );
}
/**
* I generate a random integer in between the given min and max, inclusive.
*/
function randRange( min, max ) {
return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}
}
```

When we run this demo, we get the following output:

Like I said above, randomness from a human perspective is very fuzzy. It's more about *feelings* than it is about mathematical probabilities. For example, having two of the *same* values generated in a row is the same exact probability of having *any two* values generated in a row. But, to a human, it stands-out—it *feels* different.

That said, looking at these side-by-side randomly generated visualizations, neither of them seem substantively different from a distribution perspective. Of course, the `Crypto`

module is significantly slower (half of which is all the Typed Array allocation cost). But, from a "feels" perspective, one isn't clearly better than the other.

All to say, when generating a random color palette, there probably wasn't any need for me to use the `Crypto`

module—I probably should have just stuck with `Math`

. It's much faster and *feels* to be just as random. I'll leave the `Crypto`

stuff to any client-side cryptography work (which I've never had to do).

Want to use code from this post? Check out the license.

## Reader Comments

How you sample the color space (RGB, HSV, HSL, Y'UV etc) could have more of an impact on the perception of the randomness than the random number generator itself. For example, see this XKCD post about the fully saturated color perception map in RGB space, which seems to be majority "green".

https://blog.xkcd.com/2010/05/03/color-survey-results/

## Post A Comment — ❤️ I'd Love To Hear From You! ❤️

Post a Comment →