All About Bitwise Operators or How to Extract Red from RGB Like a Pro

In the realm of bytes and bits, understanding bitwise operators can elevate your prowess in coding and data manipulation. This article explores the practical applications of bitwise operations: from packing and unpacking data, like extracting color channels from RGB values, to working with flags and bitmasks. We'll also dive into using shift operators for fast multiplication and division, and uncover clever tricks for manipulating ASCII characters.

Data Packing and Unpacking

This is common if you need to cram multiple smaller values into a single larger one to save space or conform to a protocol.

Let's explore a simple example of packing a date (Day, Month, Year) into a single 16-bit short integer. To achieve this, we need to carefully allocate the available bits to accommodate each value:

The goal is to pack the date values into a single integer:

let year = 25;  // 2025
let month = 5;  // May
let day = 31;
let packedDate = (year << 9) | (month << 5) | day;
console.log(Number(packedDate).toString(2));
// 11001010111111

We're shifting the year value (25) 9 bits to the left using the << left-shift operator. This effectively moves the least significant bit of the year value (the 1s) out of the way, making room for the month and day values: year << 9 becomes 0b100000000 (a 16-bit binary representation with the year value shifted to the left).

We're shifting the month value (5) 5 bits to the left using the << operator. This allows us to fit the month value into the available space: month << 5 becomes 0b00001000 (a 4-bit binary representation with the month value shifted to the left).

We're not shifting the day value (31) at all, as it's already a small value that can be represented using the remaining bits.

The | operator is used for bitwise OR, which combines the values:

The resulting packed date integer is 0b11001010111111, which represents the original date values. When you convert this packed date to a binary string using toString(2), you get: 11001010111111

Extracting Color Channels From a 32-bit RGB value

Colors are often represented as RGB values, where three numbers define the intensity of red, green, and blue. These values typically range from 0 (representing no color contribution) to 255 (maximum intensity), which is equivalent to a binary representation using exactly 8 bits (the decimal number 255 is equal to 11111111 in binary notation).

For example, a color might be written in hexadecimal as 0xAABBCC, where:

The corresponding binary representation looks like this: 00000000 10101010 10111011 11001100.

Here, the first 8 bits (all zeros) are often unused or represent an alpha channel (transparency) in ARGB formats. The next 8 bits (10101010, or 0xAA) are red, followed by green (10111011, or 0xBB), and blue (11001100, or 0xCC).

So how extract the red value 0xAA using bitwise operators?

let rgb = 0xAABBCC;
let red = (rgb >> 16) & 0xFF;

In the example above (JavaScript), we first defined the RGB value as a hexadecimal number (prefix 0x) and used the right-shift (>> 16) operator to move all bits in rgb to the right by 16 positions.

Shifting right by 16 bits moves the red component to the least significant byte: 00000000 00000000 00000000 10101010.

This is 0xAA - exactly the red value we want. The green and blue bits have been shifted out, and zeros fill in from the left.

The & (Bitwise AND) operator performs a bit-by-bit comparison between two numbers, yielding 1 only when both bits are 1 (& 0xFF): This ensures that you're capturing exactly one byte.

Below you will find a function called unpackRgb(packed: number) in TypeScript which extracts the individual Red, Green, and Blue channels from a 24-bit RGB value. The extracted channels are returned as an array of three integers.

/**
 * Unpacks a single integer into RGB components.
 * @param packed - Packed RGB value
 * @returns [red, green, blue] components
 */
function unpackRgb(packed: number): [number, number, number] {
    const red = (packed >> 16) & 0xff;
    const green = (packed >> 8) & 0xff;
    const blue = packed & 0xff;
    return [red, green, blue];
}

Working with Flags and Bitmasks

A bitmask is a technique used to efficiently represent multiple binary states within a single integer value. This method is often more memory-efficient and faster than storing each state as an individual boolean variable. We will use shift operators for creating and checking flags stored within the bitmask.

Let's say you have several boolean states (on/off) for an object:

const FLAG_VISIBLE = 1 << 0; // 1 (0001)
const FLAG_SOLID   = 1 << 1; // 2 (0010)
const FLAG_MOVABLE = 1 << 2; // 4 (0100)
const FLAG_PLAYER  = 1 << 3; // 8 (1000)

By using 1 << N, you are creating a value where only the N-th bit is set to 1. This guarantees each flag has a unique bit and they won't interfere with each other. You can then combine these flags using bitwise OR.

// Create a movable player object
let objectState = FLAG_PLAYER | FLAG_MOVABLE; // 8 | 4 = 12 (1100)

// To check if the object is movable:
if (objectState & FLAG_MOVABLE) {
    // This will be true
}

// To check if it's solid:
if (objectState & FLAG_SOLID) {
    // This will be false
}

To unset the FLAG_MOVABLE flag from the objectState variable, you can use the bitwise NOT operator (~) or the bitwise AND operator with a complemented mask (& ~FLAG_MOVABLE).

let newObjectState = objectState & ~FLAG_MOVABLE;

Fast Multiplication and Division by Powers of Two

This is one of the most well-known uses of shift operators. In low-level programming or performance-critical code, shifting bits can be significantly faster than performing actual multiplication or division CPU instructions.

Note: For negative numbers, the behavior of the right shift (>>) can vary. An arithmetic right shift preserves the sign bit, while a logical right shift fills with zeros. Most modern languages use an arithmetic shift for signed integers.

The JavaScript JIT Compiler

The crucial factor to consider is that JavaScript code does not run directly on the CPU. Instead, it is executed by sophisticated engines such as V8 (in Chrome and Node.js) or SpiderMonkey (in Firefox). These engines incorporate Just-In-Time (JIT) compilers.

These JIT compilers analyze code in real-time, they optimize "hot paths" (code that is executed frequently). When encountering x * 2, the JIT compiler typically converts it into the highly efficient shift instruction, a classic optimization known as Strength Reduction.

Because the JIT compiler does this automatically, manually changing x * 2 to x << 1 has several potential outcomes:

The Major Pitfall: Bitwise Operators and Numbers in JavaScript

In JavaScript, all numbers are internally represented as 64-bit floating-point numbers (doubles). However, bitwise operations only work on 32-bit signed integers.

When you use a shift operator (<< or >>), JavaScript implicitly does the following:

When working with numbers, it's best to use the right operations for the job. Avoid using shift operators for floating-point math or other situations where precision is important. Instead, stick with standard arithmetic.

Bonus: Bitwise Tricks Beyond Colors

Uppercase / Lowercase Letter Transformation

Here’s a neat trick for converting uppercase letters to lowercase in ASCII: The ASCII code for a capital letter (e.g., A, 0x41) can be transformed into its lowercase version (a, 0x61) by setting the 6th bit, which you can do with a bitwise OR (or simple addition):

let bigLetter = 'A'; // 0x41 in ASCII
let smallLetter = String.fromCharCode(bigLetter.charCodeAt(0) | 0x20); 
// Result: 'a'

This works because, in ASCII, lowercase letters are exactly 32 (0x20) greater than their uppercase counterparts. The | 0x20 operation sets the bit that makes A into a. Alternatively, you could just add 0x20:

let smallLetter = String.fromCharCode(bigLetter.charCodeAt(0) + 0x20);

To go from lowercase to uppercase, we need to clear the 6th bit (counting from 0), which can be done using a bitwise AND (&) with the complement of 0x20 (i.e., ~0x20).

let smallLetter = 'a'; // 0x61 in ASCII
// Using bitwise AND
let bigLetter = String.fromCharCode(smallLetter.charCodeAt(0) & ~0x20); // 'A'
// Using subtraction
let bigLetterAlt = String.fromCharCode(smallLetter.charCodeAt(0) - 0x20); // 'A'

Swap Variables without using a temporary variable

You can swap two variables without using a temporary variable with the bitwise XOR operator (^).

let a = 2;
let b = 3;
a ^= b;
b ^= a;
a ^= b;
console.log(a, b); // 3, 2

The key property of the XOR operator that makes this work is:

And here is the breakdown how this works for swapping the variables:

Initial values: a = 2 (binary 10) b = 3 (binary 11)

Step 1: a ^= b; This is equivalent to a = a ^ b;

Step 2: b ^= a; This is equivalent to b = b ^ a;

Remember the current values: a = 1 (binary 01), b = 3 (binary 11).

Step 3: a ^= b; This is equivalent to a = a ^ b;

Summarized:

  1. a = a ^ b (a now holds the XOR sum of original a and b)
  2. b = b ^ a (substitute a from step 1): b = b ^ (original_a ^ original_b). Since b ^ b = 0, this simplifies to b = original_a ^ (b ^ b) which means b now holds original_a.
  3. a = a ^ b (substitute a from step 1 and b from step 2): a = (original_a ^ original_b) ^ original_a. Since original_a ^ original_a = 0, this simplifies to a = original_b.
Neo - binary operators - RGB color channelsStep into the Matrix and harness the power of binary operators!