Calculating beer color in JS
Beer. Is there anybody who doesn't like it? Light and dark, hoppy and wheat, bitter and sweet. Everyone can find something for themselves.
I'm a home-brewer. Each time I brew, I make notes about the ingredients I used, mashing temperatures and hopping additions. I keep all my recipes on a webpage where I also calculate parameters like percentage of alcohol, bitterness and efficiency.
Recently I added automatic calculation of beer color, based on types of malts added. The result of this calculation is a HEX value which I put as a background color of a div containing a transparent beer glass. It looks like this:
Now, let's calculate the color of one of Saisons I made.
Saison color calculation
Each malt has it's own color defined in SRM unit. The range is from 1 to 40, the lower the value is, the lighter the color.
Based on that I created a color mapper with HEX value for each SRM color:
const COLOR_MAP = {
1: '#FFE699', 2: '#FFD878',
3: '#FFCA5A', 4: '#FFBF42',
5: '#FBB123', 6: '#F8A600',
7: '#F39C00', 8: '#EA8F00',
...,
39: '#3A070B', 40: '#36080A',
'max': '#030403'
};
The base pattern for calculating beer SRM color is:
SRM = 1.4922 * (MCU ^ 0.6859)
where MCU is Malt Color Unit.
To calculate MCU of the whole beer, we need to use this pattern:
MCU = (Weight of grains in lbs) * (Color of grains) / (Batch volume in gallons)
As you can see, the above pattern uses lb and gallon units, and I mostly use Polish kilograms and litres. So I had to write converters for them:
const CONVERTERS = {
'kg2lbs': 2.20462262,
'g2lbs': 0.00220462262,
'l2gal': 0.264172052
};
Taking all this into account, we can create a prototype of our main function:
function getBeerColor(batchSize, grains) {
// calculation of MCU from all used grains
let mcu = calculateMCU(grains);
// converting batch size from litres to gallons
let batchSize = getBatchSizeInGallons(batchSize);
// calculating color
let color = calculateColor(mcu, batchSize);
// converting SRM color into HEX value
let hex = convertSrmToHex(color);
return hex;
}
Malt Color Units calculation
To calculate MCU for this beer, we have to create an array of malts, where each malt should be an object with 2 keys - amount
and color
. To have this info, we need to take a deeper look into the recipe:
2.1 kg Pilzen malt [srm: 3]
1.0 kg Pale Ale [srm: 3]
0.6 kg Wheat malt [srm: 2]
0.41 kg Munich malt [srm: 9]
0.2 kg Carahell [srm: 20]
0.325 kg Caramel 30L [srm: 30]
So our grain collection looks like this:
var grains = [
{amount: 2.1, srm: 3},
{amount: 1.0, srm: 3},
{amount: 0.6, srm: 2},
{amount: 0.41, srm: 9},
{amount: 0.2, srm: 20},
{amount: 0.325, srm: 30}
];
Now we can create a function which calculates MCU for our beer:
function calculateMCU(grains) {
let mcu = 0,
amount,
color;
let factor = CONVERTERS['kg2lbs'];
for (let i = 0; i < grains.length; i++) {
amount = grains[i].amount * factor;
color = grains[i].color || 0;
mcu += amount * color;
}
return mcu;
}
Please note, that in the original MCU pattern the grain-color value is divided by batch size in gallons. At this point I don't have this value calculated, so I will do it later.
Batch size conversion
Recipe is calculated for 21 litres. It means that we have to convert this value into gallons, to make further calculations. To convert it I'll use mapper which I created earlier:
function getBatchSizeInGallons(amount) {
let factor = CONVERTERS[`l2gal`];
return amount * factor;
}
Half of the work done. Let's go further.
Calculating SRM color
Having needed 2 values for calculating SRM color of the beer, we can write a function for it:
function calculateColor(mcu, batchSize) {
let color = 0;
if (mcu && batchSize) {
color = 1.4922 * Math.pow(mcu / batchSize, 0.6859);
}
return color;
}
As you can see, here is the place where I put batchSize
instead of MCU calculation which is in the original pattern. As a result of this function, we receive SRM color of this beer, which parsed to integer gives us 12. The only thing we have to do now, is to convert it into HEX.
Converting color
For SRM to HEX conversion we will use the mapper I created eariler. The function just takes SRM color as a parameter and converts it. If SRM is higher than 40, it takes maximum value from COLOR_MAP
object.
function convertSrmToHex(srm) {
let color = Math.floor(srm);
let hex = color && color > 40 ? COLOR_MAP.max : COLOR_MAP[color];
return hex || '';
}
Result
We now have all the needed parts of color calculation. The result of executing function getBeerColor
is HEX value #cf6900
. All we need to do now is to put this color as a background of a glass. And it looks like this:
Nice, huh?
Closing words
There are a lot of fun calculations during homebrewing. You can calculate bitterness (IBU), your efficiency, percentage of alcohol and many more.
And of course while doing that, you can drink something that you created by yourself.
Cheers!