Skip to content

Instantly share code, notes, and snippets.

@ericviana
Created July 24, 2025 21:52
Show Gist options
  • Select an option

  • Save ericviana/7cfd3af162655b98dac46a0d96fad818 to your computer and use it in GitHub Desktop.

Select an option

Save ericviana/7cfd3af162655b98dac46a0d96fad818 to your computer and use it in GitHub Desktop.
Pure bash script to convert ALL hex colors to OKLCH in CSS files
#!/bin/bash
# hex-to-oklch-converter.sh
# Pure bash script to convert ALL hex colors to OKLCH in CSS files
set -e
# Function to show usage
usage() {
echo "Usage: $0 <css-file>"
echo "Example: $0 styles.css"
echo ""
echo "This script will:"
echo "- Find ALL hex colors in CSS files"
echo "- Convert them to OKLCH format with proper alpha handling"
echo "- Handle 8-digit hex colors with alpha"
echo "- Update the file in place"
echo "- Create a backup with timestamp"
exit 1
}
# Check if bc is available
if ! command -v bc &>/dev/null; then
echo "Error: 'bc' calculator is required but not installed."
echo "Please install bc: sudo apt-get install bc (Ubuntu/Debian) or brew install bc (macOS)"
exit 1
fi
# Check if file argument is provided
if [ $# -eq 0 ]; then
usage
fi
CSS_FILE="$1"
# Check if file exists
if [ ! -f "$CSS_FILE" ]; then
echo "Error: File '$CSS_FILE' not found!"
exit 1
fi
# Function to convert hex to decimal
hex_to_dec() {
echo "ibase=16; $1" | bc
}
# Function for cube root using bc
cbrt() {
local num="$1"
if (($(echo "$num <= 0" | bc -l))); then
echo "0"
else
echo "scale=10; e(l($num)/3)" | bc -l
fi
}
# Function for square root using bc
sqrt() {
echo "scale=10; sqrt($1)" | bc -l
}
# Function for atan2 using bc
atan2() {
local y="$1"
local x="$2"
if (($(echo "$x == 0 && $y == 0" | bc -l))); then
echo "0"
return
fi
if (($(echo "$x > 0" | bc -l))); then
echo "scale=10; a($y/$x)" | bc -l
elif (($(echo "$x < 0 && $y >= 0" | bc -l))); then
echo "scale=10; a($y/$x) + 3.14159265359" | bc -l
elif (($(echo "$x < 0 && $y < 0" | bc -l))); then
echo "scale=10; a($y/$x) - 3.14159265359" | bc -l
elif (($(echo "$x == 0 && $y > 0" | bc -l))); then
echo "1.57079632679" # π/2
else
echo "-1.57079632679" # -π/2
fi
}
# Function to convert radians to degrees
rad_to_deg() {
echo "scale=6; $1 * 180 / 3.14159265359" | bc -l
}
# Function to convert sRGB to linear RGB
srgb_to_linear() {
local val=$(echo "scale=10; $1 / 255" | bc -l)
if (($(echo "$val <= 0.04045" | bc -l))); then
echo "scale=10; $val / 12.92" | bc -l
else
echo "scale=10; e(2.4 * l(($val + 0.055) / 1.055))" | bc -l
fi
}
# Main function to convert hex to OKLCH
hex_to_oklch() {
local original_hex="$1"
local hex="$1"
local alpha=""
# Remove # if present
hex=$(echo "$hex" | sed 's/^#//')
# Handle 8-digit hex colors (extract alpha)
if [ ${#hex} -eq 8 ]; then
alpha="${hex:6:2}"
hex="${hex:0:6}"
fi
# Ensure we have 6 characters for RGB
if [ ${#hex} -ne 6 ]; then
echo "Invalid hex color: $original_hex" >&2
return 1
fi
# Extract RGB components (uppercase for bc)
local r_hex=$(echo "${hex:0:2}" | tr '[:lower:]' '[:upper:]')
local g_hex=$(echo "${hex:2:2}" | tr '[:lower:]' '[:upper:]')
local b_hex=$(echo "${hex:4:2}" | tr '[:lower:]' '[:upper:]')
# Convert to decimal
local r_dec=$(hex_to_dec "$r_hex")
local g_dec=$(hex_to_dec "$g_hex")
local b_dec=$(hex_to_dec "$b_hex")
# Convert to linear RGB
local r_linear=$(srgb_to_linear "$r_dec")
local g_linear=$(srgb_to_linear "$g_dec")
local b_linear=$(srgb_to_linear "$b_dec")
# Convert linear RGB to OKLab
local l=$(echo "scale=10; 0.4122214708 * $r_linear + 0.5363325363 * $g_linear + 0.0514459929 * $b_linear" | bc -l)
local m=$(echo "scale=10; 0.2119034982 * $r_linear + 0.6806995451 * $g_linear + 0.1073969566 * $b_linear" | bc -l)
local s=$(echo "scale=10; 0.0883024619 * $r_linear + 0.2817188376 * $g_linear + 0.6299787005 * $b_linear" | bc -l)
# Take cube roots
local l_cbrt=$(cbrt "$l")
local m_cbrt=$(cbrt "$m")
local s_cbrt=$(cbrt "$s")
# Convert to OKLab
local L=$(echo "scale=10; 0.2104542553 * $l_cbrt + 0.7936177850 * $m_cbrt - 0.0040720468 * $s_cbrt" | bc -l)
local a=$(echo "scale=10; 1.9779984951 * $l_cbrt - 2.4285922050 * $m_cbrt + 0.4505937099 * $s_cbrt" | bc -l)
local b_lab=$(echo "scale=10; 0.0259040371 * $l_cbrt + 0.7827717662 * $m_cbrt - 0.8086757660 * $s_cbrt" | bc -l)
# Convert OKLab to OKLCH
local C=$(sqrt "$(echo "scale=10; $a * $a + $b_lab * $b_lab" | bc -l)")
local H_rad=$(atan2 "$b_lab" "$a")
local H=$(rad_to_deg "$H_rad")
# Ensure H is positive
if (($(echo "$H < 0" | bc -l))); then
H=$(echo "scale=6; $H + 360" | bc -l)
fi
# Format output with proper precision
local L_percent=$(echo "scale=1; $L * 100" | bc -l)
local C_formatted=$(printf "%.4f" "$C")
local H_formatted=$(printf "%.1f" "$H")
# Remove trailing zeros and format
L_percent=$(echo "$L_percent" | sed 's/\.0$//')
H_formatted=$(echo "$H_formatted" | sed 's/\.0$//')
# Handle alpha channel
if [ -n "$alpha" ]; then
local alpha_decimal=$(hex_to_dec "$(echo "$alpha" | tr '[:lower:]' '[:upper:]')")
local alpha_percent=$(echo "scale=1; $alpha_decimal / 255 * 100" | bc -l)
# Format alpha percentage properly
alpha_percent=$(printf "%.1f" "$alpha_percent")
alpha_percent=$(echo "$alpha_percent" | sed 's/\.0$//')
echo "oklch(${L_percent}% ${C_formatted} ${H_formatted} / ${alpha_percent}%)"
else
echo "oklch(${L_percent}% ${C_formatted} ${H_formatted})"
fi
}
# Create backup with timestamp and proper extension
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${CSS_FILE%.css}_backup_${TIMESTAMP}.css"
cp "$CSS_FILE" "$BACKUP_FILE"
echo "Created backup: $BACKUP_FILE"
# Process the file
echo "Converting hex colors to OKLCH..."
# Create a temporary file for processing
temp_file=$(mktemp)
# Process line by line with better hex color detection
while IFS= read -r line || [ -n "$line" ]; do
processed_line="$line"
# Find hex colors using a more precise regex
# Match #rrggbb or #rrggbbaa patterns
if echo "$line" | grep -qE '#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?'; then
# Extract all hex colors from the line
hex_matches=$(echo "$line" | grep -oE '#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?' || true)
for hex_color in $hex_matches; do
if [ -n "$hex_color" ]; then
oklch_value=$(hex_to_oklch "$hex_color" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$oklch_value" ]; then
# Use a more precise replacement to avoid partial matches
processed_line=$(echo "$processed_line" | sed "s|${hex_color}|${oklch_value}|g")
echo " Converted: $hex_color → $oklch_value" >&2
fi
fi
done
fi
echo "$processed_line"
done <"$CSS_FILE" >"$temp_file"
# Replace original file with processed version
mv "$temp_file" "$CSS_FILE"
echo ""
echo "Conversion complete!"
echo "Original file backed up as: $BACKUP_FILE"
echo "Hex colors have been converted to OKLCH format in: $CSS_FILE"
# Count how many conversions were made
conversion_count=$(diff "$BACKUP_FILE" "$CSS_FILE" | grep -c "oklch(" || echo "0")
echo "Total conversions made: $conversion_count"
# Show a summary of changes
echo ""
echo "Summary of changes (first 10 lines):"
if command -v diff &>/dev/null; then
diff "$BACKUP_FILE" "$CSS_FILE" | grep "oklch(" | head -10 || echo "No changes detected"
fi
echo ""
echo "To verify alpha conversion:"
echo "3D hex = $(hex_to_dec 3D) decimal = $(echo "scale=1; $(hex_to_dec 3D) / 255 * 100" | bc -l)% opacity"
echo ""
echo "To view the backup file, use:"
echo " cat $BACKUP_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment