Skip to content

Instantly share code, notes, and snippets.

@Merovex
Created May 13, 2025 18:46
Show Gist options
  • Save Merovex/226e4272863ac8652340d5dc7ad25d60 to your computer and use it in GitHub Desktop.
Save Merovex/226e4272863ac8652340d5dc7ad25d60 to your computer and use it in GitHub Desktop.
The Ruby code generates Material Design 3 color schemes from a source color, calculating harmonious palettes with proper contrast for light and dark themes.
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'json'
##
# Material Design 3 Color Generator
#
# A Ruby implementation of the Material Design 3 dynamic color system.
# Generates color schemes and palettes based on a source color.
#
# Usage:
# ruby material_design_3.rb <hex_color>
# ruby material_design_3.rb 6750A4
#
# Options:
# --json Output JSON only
# --css Output CSS only
# --files Generate CSS and JSON files (default)
#
# Author: Claude
##
class MaterialDesign3ColorGenerator
# Color Conversion Utilities
# Convert a hex color to RGB array
def self.hex_to_rgb(hex)
# Remove the # if present
hex = hex.gsub(/^#/, '')
# Parse the hex value
r = hex[0..1].to_i(16)
g = hex[2..3].to_i(16)
b = hex[4..5].to_i(16)
[r, g, b]
end
# Convert RGB array to hex
def self.rgb_to_hex(rgb)
"##{rgb.map { |c| format('%02X', c.round.clamp(0, 255)) }.join}"
end
# Convert RGB (0-255) to sRGB (0-1)
def self.rgb_to_srgb(rgb)
rgb.map { |c| c / 255.0 }
end
# Convert sRGB to linear RGB
def self.srgb_to_linear(srgb)
srgb.map do |c|
if c <= 0.04045
c / 12.92
else
((c + 0.055) / 1.055) ** 2.4
end
end
end
# Convert linear RGB to sRGB
def self.linear_to_srgb(rgb)
rgb.map do |c|
if c <= 0.0031308
c * 12.92
else
1.055 * (c ** (1.0 / 2.4)) - 0.055
end
end
end
# Convert linear RGB to XYZ
def self.linear_rgb_to_xyz(rgb)
# sRGB to XYZ matrix
matrix = [
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
]
matrix.map do |row|
row[0] * rgb[0] + row[1] * rgb[1] + row[2] * rgb[2]
end
end
# Convert XYZ to linear RGB
def self.xyz_to_linear_rgb(xyz)
# XYZ to sRGB matrix
matrix = [
[ 3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[ 0.0556434, -0.2040259, 1.0572252]
]
matrix.map do |row|
row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2]
end
end
# Convert XYZ to LAB
def self.xyz_to_lab(xyz)
# D65 white point reference
xn = 0.95047
yn = 1.0
zn = 1.08883
# Normalize XYZ
x = xyz[0] / xn
y = xyz[1] / yn
z = xyz[2] / zn
# Apply cube root function
x = x > 0.008856 ? x ** (1.0 / 3) : (7.787 * x) + (16.0 / 116)
y = y > 0.008856 ? y ** (1.0 / 3) : (7.787 * y) + (16.0 / 116)
z = z > 0.008856 ? z ** (1.0 / 3) : (7.787 * z) + (16.0 / 116)
# Calculate L, a, b
l = (116 * y) - 16
a = 500 * (x - y)
b = 200 * (y - z)
[l, a, b]
end
# Convert LAB to XYZ
def self.lab_to_xyz(lab)
# D65 white point reference
xn = 0.95047
yn = 1.0
zn = 1.08883
l, a, b = lab
# Calculate y
y = (l + 16) / 116.0
# Calculate x
x = a / 500.0 + y
# Calculate z
z = y - b / 200.0
# Invert the cube root function
x = x ** 3 > 0.008856 ? x ** 3 : (x - 16.0 / 116) / 7.787
y = y ** 3 > 0.008856 ? y ** 3 : (y - 16.0 / 116) / 7.787
z = z ** 3 > 0.008856 ? z ** 3 : (z - 16.0 / 116) / 7.787
[x * xn, y * yn, z * zn]
end
# Convert LAB to LCH
def self.lab_to_lch(lab)
l, a, b = lab
# Calculate chroma
c = Math.sqrt(a * a + b * b)
# Calculate hue in radians, then convert to degrees
h = Math.atan2(b, a) * (180 / Math::PI)
# Make sure hue is between 0 and 360
h += 360 if h < 0
[l, c, h]
end
# Convert LCH to LAB
def self.lch_to_lab(lch)
l, c, h = lch
# Convert hue to radians
h_rad = h * (Math::PI / 180)
# Calculate a and b
a = c * Math.cos(h_rad)
b = c * Math.sin(h_rad)
[l, a, b]
end
# HCT Color Space Implementation
# Convert RGB to HCT with better handling of saturation
def self.rgb_to_hct(rgb)
srgb = rgb_to_srgb(rgb)
linear_rgb = srgb_to_linear(srgb)
xyz = linear_rgb_to_xyz(linear_rgb)
lab = xyz_to_lab(xyz)
lch = lab_to_lch(lab)
# Extract base components
h = lch[2]
c = lch[1]
l = lch[0]
# Handle saturation better for different hue ranges
# Yellow-orange-red colors (hues 15-60) often need more saturation
if h >= 15 && h <= 60
c *= 1.2 # Boost chroma for yellows/oranges
# Greens (hues 80-160) sometimes appear less saturated
elsif h >= 80 && h <= 160
c *= 1.15 # Moderate boost for greens
# Cyans and blues (hues 170-250) generally appear more saturated
elsif h >= 170 && h <= 250
c *= 1.05 # Slight boost for blues
end
# Return HCT components
{
hue: h,
chroma: c,
tone: l
}
end
# Convert HCT to RGB with improved saturation preservation
def self.hct_to_rgb(hct)
# Start with the target HCT
hue = hct[:hue]
chroma = hct[:chroma]
tone = hct[:tone]
# Ensure hue is 0-360
hue = ((hue % 360) + 360) % 360
# Ensure tone is 0-100
tone = [0, [100, tone].min].max
# Skip conversion for pure black and white
return [0, 0, 0] if tone <= 0.5
return [255, 255, 255] if tone >= 99.5
# Convert to LCH
lch = [tone, chroma, hue]
# First try with the requested chroma
lab = lch_to_lab(lch)
xyz = lab_to_xyz(lab)
linear_rgb = xyz_to_linear_rgb(xyz)
srgb = linear_to_srgb(linear_rgb)
# Check if the color is within the sRGB gamut with small tolerance
in_gamut = srgb.all? { |c| c >= -0.001 && c <= 1.001 }
if in_gamut
# Return the color as RGB
return srgb.map { |c| (c.clamp(0, 1) * 255).round }
end
# If out of gamut, we need to preserve as much saturation as possible
# First, try a more aggressive approach to retain saturation
# Find the maximum in-gamut chroma through binary search
lo = 0.0
hi = chroma
best_chroma = 0
best_rgb = [0, 0, 0]
16.times do # Increased iterations for more precision
mid = (lo + hi) / 2.0
new_lch = [tone, mid, hue]
new_lab = lch_to_lab(new_lch)
new_xyz = lab_to_xyz(new_lab)
new_linear_rgb = xyz_to_linear_rgb(new_xyz)
new_srgb = linear_to_srgb(new_linear_rgb)
if new_srgb.all? { |c| c >= -0.001 && c <= 1.001 }
# This chroma is in-gamut, update best and try higher
best_chroma = mid
best_rgb = new_srgb.map { |c| (c.clamp(0, 1) * 255).round }
lo = mid
else
hi = mid
end
end
# Sometimes we can preserve more saturation by slightly adjusting the hue
# Try adjusting hue slightly in both directions to find better saturation
[-10, -5, 5, 10].each do |hue_shift|
adjusted_hue = (hue + hue_shift) % 360
# Start with a high chroma and reduce until in gamut
test_chroma = chroma * 0.95 # Start a bit below original to avoid too many iterations
6.times do # Just a few iterations to see if we can do better
test_lch = [tone, test_chroma, adjusted_hue]
test_lab = lch_to_lab(test_lch)
test_xyz = lab_to_xyz(test_lab)
test_linear_rgb = xyz_to_linear_rgb(test_xyz)
test_srgb = linear_to_srgb(test_linear_rgb)
if test_srgb.all? { |c| c >= -0.001 && c <= 1.001 }
# Found a better chroma with this hue shift
if test_chroma > best_chroma
best_chroma = test_chroma
best_rgb = test_srgb.map { |c| (c.clamp(0, 1) * 255).round }
end
break
end
# Reduce chroma and try again
test_chroma *= 0.8
end
end
best_rgb
end
# Material Design 3 Color Scheme Generation
# Create a tonal palette from an HCT color with enhanced saturation retention
def self.create_tonal_palette(hct)
# MD3 uses these tone levels for a palette (including more granular levels)
tones = [
0, 4, 5, 6, 10, 12, 15, 17,
20, 22, 24, 25, 30, 35, 40,
50, 60, 70, 80, 87, 90, 92, 95, 96,
98, 99, 100
]
# Create the palette
palette = {}
# Enhanced saturation retention factors
# Optimized from analysis of actual MD3 palettes
saturation_retention = {
4 => 0.25,
5 => 0.35,
6 => 0.40,
10 => 0.45,
12 => 0.50,
17 => 0.50,
15 => 0.55,
20 => 0.65,
22 => 0.70,
24 => 0.70,
25 => 0.75,
30 => 0.82,
35 => 0.75,
40 => 0.80,
50 => 0.85,
60 => 0.88,
70 => 0.88,
80 => 0.85,
87 => 0.80,
90 => 0.78,
92 => 0.75,
95 => 0.68,
96 => 0.65,
98 => 0.85,
99 => 0.75
}
# We start with black and white for tones 0 and 100
palette[0] = "#000000"
palette[100] = "#FFFFFF"
# Generate the rest of the tones
(tones - [0, 100]).each do |tone|
# Calculate how much of the original saturation to preserve
saturation_factor = saturation_retention[tone] || 0.5
# For warm colors (yellows, oranges, reds), boost saturation
hue = hct[:hue]
is_warm = (hue >= 20 && hue <= 80) || (hue >= 330)
# Apply more restrained saturation boost for high tones
saturation_boost = if is_warm && (tone == 80 || tone == 90)
# More conservative boost for tones 80 and 90
0.9
elsif is_warm
1.3
else
1.1
end
# Calculate actual chroma to use
target_chroma = hct[:chroma] * saturation_factor * saturation_boost
# Create new HCT color preserving the hue and using target chroma
new_hct = {
hue: hct[:hue],
chroma: target_chroma,
tone: tone
}
# Convert to RGB
rgb = hct_to_rgb(new_hct)
# Convert to hex (uppercase for MD3 compliance)
palette[tone] = rgb_to_hex(rgb)
end
palette
end
# Generate a Material Design 3 color scheme from a source color
def self.generate_material_scheme(source_color_hex)
# Convert the source color to HCT
rgb = hex_to_rgb(source_color_hex)
hct = rgb_to_hct(rgb)
# Boost chroma to match MD3's vibrant colors
hct[:chroma] *= 1.5
# Create the primary tonal palette
primary_palette = create_tonal_palette(hct)
# Determine secondary hue shift based on primary color warmth
primary_hue = hct[:hue]
is_warm_color = (primary_hue >= 20 && primary_hue <= 180)
# Apply a small hue shift in the appropriate direction
secondary_hue_shift = is_warm_color ? -7 : +4 # Clockwise for warm, counterclockwise for cool
# Create a secondary palette with adjusted hue and reduced chroma
secondary_hct = {
hue: (primary_hue + secondary_hue_shift) % 360,
chroma: hct[:chroma] * 0.27, # Reduced to 27% of primary's chroma
tone: hct[:tone]
}
secondary_palette = create_tonal_palette(secondary_hct)
# Create a tertiary palette with 60° hue shift
tertiary_hct = {
hue: (primary_hue + 60) % 360, # Consistent 60° counterclockwise shift
chroma: hct[:chroma] * 0.3, # Reduced to 30% of primary's chroma
tone: hct[:tone] + 2 # Slight increase in tone/lightness
}
tertiary_palette = create_tonal_palette(tertiary_hct)
# Create neutral palettes with very low chroma
neutral_hct = {
hue: hct[:hue],
chroma: [hct[:chroma] * 0.12, 4].min,
tone: 50
}
neutral_palette = create_tonal_palette(neutral_hct)
neutral_variant_hct = {
hue: hct[:hue],
chroma: [hct[:chroma] * 0.2, 8].min,
tone: 50
}
neutral_variant_palette = create_tonal_palette(neutral_variant_hct)
# Create error palette (red)
error_hct = {
hue: 25, # Red hue
chroma: 100, # Vibrant red for error states
tone: 50
}
error_palette = create_tonal_palette(error_hct)
# Return the scheme with both light and dark variants
{
source: source_color_hex,
palettes: {
primary: primary_palette,
secondary: secondary_palette,
tertiary: tertiary_palette,
neutral: neutral_palette,
neutral_variant: neutral_variant_palette,
error: error_palette
},
light: {
primary: primary_palette[40],
on_primary: primary_palette[100],
primary_container: primary_palette[90],
on_primary_container: primary_palette[10],
secondary: secondary_palette[40],
on_secondary: secondary_palette[100],
secondary_container: secondary_palette[90],
on_secondary_container: secondary_palette[10],
tertiary: tertiary_palette[40],
on_tertiary: tertiary_palette[100],
tertiary_container: tertiary_palette[90],
on_tertiary_container: tertiary_palette[10],
error: error_palette[40],
on_error: error_palette[100],
error_container: error_palette[90],
on_error_container: error_palette[10],
background: neutral_palette[99],
on_background: neutral_palette[10],
surface: neutral_palette[99],
on_surface: neutral_palette[10],
surface_variant: neutral_variant_palette[90],
on_surface_variant: neutral_variant_palette[30],
outline: neutral_variant_palette[50],
shadow: neutral_palette[0],
inverse_surface: neutral_palette[20],
inverse_on_surface: neutral_palette[95],
inverse_primary: primary_palette[80],
surface_dim: neutral_palette[87],
surface_bright: neutral_palette[98],
surface_container_lowest: neutral_palette[100],
surface_container_low: neutral_palette[96],
surface_container: neutral_palette[94],
surface_container_high: neutral_palette[92],
surface_container_highest: neutral_palette[90]
},
dark: {
primary: primary_palette[80],
on_primary: primary_palette[20],
primary_container: primary_palette[30],
on_primary_container: primary_palette[90],
secondary: secondary_palette[80],
on_secondary: secondary_palette[20],
secondary_container: secondary_palette[30],
on_secondary_container: secondary_palette[90],
tertiary: tertiary_palette[80],
on_tertiary: tertiary_palette[20],
tertiary_container: tertiary_palette[30],
on_tertiary_container: tertiary_palette[90],
error: error_palette[80],
on_error: error_palette[20],
error_container: error_palette[30],
on_error_container: error_palette[90],
background: neutral_palette[10],
on_background: neutral_palette[90],
surface: neutral_palette[10],
on_surface: neutral_palette[90],
surface_variant: neutral_variant_palette[30],
on_surface_variant: neutral_variant_palette[80],
outline: neutral_variant_palette[60],
shadow: neutral_palette[0],
inverse_surface: neutral_palette[90],
inverse_on_surface: neutral_palette[20],
inverse_primary: primary_palette[40],
surface_dim: neutral_palette[6],
surface_bright: neutral_palette[24],
surface_container_lowest: neutral_palette[4],
surface_container_low: neutral_palette[10],
surface_container: neutral_palette[12],
surface_container_high: neutral_palette[17],
surface_container_highest: neutral_palette[22]
}
}
end
# Generate and return JSON for a given hex color
def self.generate_json(hex_color)
scheme = generate_material_scheme(hex_color)
JSON.pretty_generate(scheme)
end
# Generate CSS file and JSON file for a given hex color
def self.create_palette_files(hex_color)
# Remove # if present
hex_color = hex_color.gsub(/^#/, '')
# Generate the color scheme
scheme = generate_material_scheme("##{hex_color}")
# Create filenames
css_filename = "color-theme-#{hex_color.downcase}.css"
json_filename = "color-theme-#{hex_color.downcase}.json"
# Generate CSS
css_content = generate_css_content(hex_color, scheme)
File.write(css_filename, css_content)
# Generate JSON
json_content = JSON.pretty_generate(scheme)
File.write(json_filename, json_content)
puts "Generated palette files:"
puts "- #{css_filename} (CSS format)"
puts "- #{json_filename} (JSON format)"
# Return the filenames for reference
[css_filename, json_filename]
end
# Generate CSS content for a color scheme
def self.generate_css_content(hex_color, scheme)
# Extract palettes and roles from the scheme
p = scheme[:palettes]
light = scheme[:light]
dark = scheme[:dark]
# Generate CSS content
css = <<~CSS
/**
* Material Design 3 Color Theme
* Generated for color: ##{hex_color.upcase}
*/
[data-color='#{hex_color.downcase}'] {
--primary-0: #{p[:primary][0]};
--primary-5: #{p[:primary][5]};
--primary-10: #{p[:primary][10]};
--primary-15: #{p[:primary][15]};
--primary-20: #{p[:primary][20]};
--primary-30: #{p[:primary][30]};
--primary-40: #{p[:primary][40]};
--primary-50: #{p[:primary][50]};
--primary-60: #{p[:primary][60]};
--primary-70: #{p[:primary][70]};
--primary-80: #{p[:primary][80]};
--primary-90: #{p[:primary][90]};
--primary-95: #{p[:primary][95]};
--primary-99: #{p[:primary][99]};
--primary-100: #{p[:primary][100]};
--secondary-0: #{p[:secondary][0]};
--secondary-5: #{p[:secondary][5]};
--secondary-10: #{p[:secondary][10]};
--secondary-15: #{p[:secondary][15]};
--secondary-20: #{p[:secondary][20]};
--secondary-30: #{p[:secondary][30]};
--secondary-40: #{p[:secondary][40]};
--secondary-50: #{p[:secondary][50]};
--secondary-60: #{p[:secondary][60]};
--secondary-70: #{p[:secondary][70]};
--secondary-80: #{p[:secondary][80]};
--secondary-90: #{p[:secondary][90]};
--secondary-95: #{p[:secondary][95]};
--secondary-99: #{p[:secondary][99]};
--secondary-100: #{p[:secondary][100]};
--tertiary-0: #{p[:tertiary][0]};
--tertiary-5: #{p[:tertiary][5]};
--tertiary-10: #{p[:tertiary][10]};
--tertiary-15: #{p[:tertiary][15]};
--tertiary-20: #{p[:tertiary][20]};
--tertiary-30: #{p[:tertiary][30]};
--tertiary-40: #{p[:tertiary][40]};
--tertiary-50: #{p[:tertiary][50]};
--tertiary-60: #{p[:tertiary][60]};
--tertiary-70: #{p[:tertiary][70]};
--tertiary-80: #{p[:tertiary][80]};
--tertiary-90: #{p[:tertiary][90]};
--tertiary-95: #{p[:tertiary][95]};
--tertiary-99: #{p[:tertiary][99]};
--tertiary-100: #{p[:tertiary][100]};
--error-0: #{p[:error][0]};
--error-5: #{p[:error][5]};
--error-10: #{p[:error][10]};
--error-15: #{p[:error][15]};
--error-20: #{p[:error][20]};
--error-30: #{p[:error][30]};
--error-40: #{p[:error][40]};
--error-50: #{p[:error][50]};
--error-60: #{p[:error][60]};
--error-70: #{p[:error][70]};
--error-80: #{p[:error][80]};
--error-90: #{p[:error][90]};
--error-95: #{p[:error][95]};
--error-99: #{p[:error][99]};
--error-100: #{p[:error][100]};
--neutral-0: #{p[:neutral][0]};
--neutral-5: #{p[:neutral][5]};
--neutral-10: #{p[:neutral][10]};
--neutral-15: #{p[:neutral][15]};
--neutral-20: #{p[:neutral][20]};
--neutral-30: #{p[:neutral][30]};
--neutral-40: #{p[:neutral][40]};
--neutral-50: #{p[:neutral][50]};
--neutral-60: #{p[:neutral][60]};
--neutral-70: #{p[:neutral][70]};
--neutral-80: #{p[:neutral][80]};
--neutral-90: #{p[:neutral][90]};
--neutral-95: #{p[:neutral][95]};
--neutral-99: #{p[:neutral][99]};
--neutral-100: #{p[:neutral][100]};
--neutral-variant-0: #{p[:neutral_variant][0]};
--neutral-variant-5: #{p[:neutral_variant][5]};
--neutral-variant-10: #{p[:neutral_variant][10]};
--neutral-variant-15: #{p[:neutral_variant][15]};
--neutral-variant-20: #{p[:neutral_variant][20]};
--neutral-variant-30: #{p[:neutral_variant][30]};
--neutral-variant-40: #{p[:neutral_variant][40]};
--neutral-variant-50: #{p[:neutral_variant][50]};
--neutral-variant-60: #{p[:neutral_variant][60]};
--neutral-variant-70: #{p[:neutral_variant][70]};
--neutral-variant-80: #{p[:neutral_variant][80]};
--neutral-variant-90: #{p[:neutral_variant][90]};
--neutral-variant-95: #{p[:neutral_variant][95]};
--neutral-variant-99: #{p[:neutral_variant][99]};
--neutral-variant-100: #{p[:neutral_variant][100]};
}
[data-color='#{hex_color.downcase}'][data-color-scheme='light'] {
--primary: #{light[:primary]};
--surface-tint: #{light[:primary]};
--on-primary: #{light[:on_primary]};
--primary-container: #{light[:primary_container]};
--on-primary-container: #{light[:on_primary_container]};
--secondary: #{light[:secondary]};
--on-secondary: #{light[:on_secondary]};
--secondary-container: #{light[:secondary_container]};
--on-secondary-container: #{light[:on_secondary_container]};
--on-secondary-container-transparent: #{light[:on_secondary_container]}14;
--tertiary: #{light[:tertiary]};
--on-tertiary: #{light[:on_tertiary]};
--tertiary-container: #{light[:tertiary_container]};
--on-tertiary-container: #{light[:on_tertiary_container]};
--error: #{light[:error]};
--on-error: #{light[:on_error]};
--error-container: #{light[:error_container]};
--on-error-container: #{light[:on_error_container]};
--background: #{light[:background]};
--on-background: #{light[:on_background]};
--surface: #{light[:surface]};
--on-surface: #{light[:on_surface]};
--surface-variant: #{light[:surface_variant]};
--on-surface-variant: #{light[:on_surface_variant]};
--outline: #{light[:outline]};
--outline-variant: #{p[:neutral_variant][80]};
--shadow: #{light[:shadow]};
--scrim: #{light[:shadow]};
--inverse-surface: #{light[:inverse_surface]};
--on-inverse-surface: #{light[:inverse_on_surface]};
--inverse-primary: #{light[:inverse_primary]};
--primary-fixed: #{light[:primary_container]};
--on-primary-fixed: #{p[:primary][10]};
--primary-fixed-dim: #{p[:primary][80]};
--on-primary-fixed-variant: #{light[:on_primary_container]};
--secondary-fixed: #{light[:secondary_container]};
--on-secondary-fixed: #{p[:secondary][10]};
--secondary-fixed-dim: #{p[:secondary][80]};
--on-secondary-fixed-variant: #{light[:on_secondary_container]};
--tertiary-fixed: #{light[:tertiary_container]};
--on-tertiary-fixed: #{p[:tertiary][10]};
--tertiary-fixed-dim: #{p[:tertiary][80]};
--on-tertiary-fixed-variant: #{light[:on_tertiary_container]};
--surface-dim: #{p[:neutral][87]};
--surface-bright: #{light[:background]};
--surface-container-lowest: #{p[:neutral][100]};
--surface-container-low: #{p[:neutral][96]};
--surface-container: #{p[:neutral][94]};
--surface-container-high: #{p[:neutral][92]};
--surface-container-highest: #{p[:neutral][90]};
}
[data-color='#{hex_color.downcase}'][data-color-scheme='dark'] {
--primary: #{dark[:primary]};
--surface-tint: #{dark[:primary]};
--on-primary: #{dark[:on_primary]};
--primary-container: #{dark[:primary_container]};
--on-primary-container: #{dark[:on_primary_container]};
--secondary: #{dark[:secondary]};
--on-secondary: #{dark[:on_secondary]};
--secondary-container: #{dark[:secondary_container]};
--on-secondary-container: #{dark[:on_secondary_container]};
--on-secondary-container-transparent: #{dark[:on_secondary_container]}14;
--tertiary: #{dark[:tertiary]};
--on-tertiary: #{dark[:on_tertiary]};
--tertiary-container: #{dark[:tertiary_container]};
--on-tertiary-container: #{dark[:on_tertiary_container]};
--error: #{dark[:error]};
--on-error: #{dark[:on_error]};
--error-container: #{dark[:error_container]};
--on-error-container: #{dark[:on_error_container]};
--background: #{dark[:background]};
--on-background: #{dark[:on_background]};
--surface: #{dark[:surface]};
--on-surface: #{dark[:on_surface]};
--surface-variant: #{dark[:surface_variant]};
--on-surface-variant: #{dark[:on_surface_variant]};
--outline: #{dark[:outline]};
--outline-variant: #{dark[:surface_variant]};
--shadow: #{dark[:shadow]};
--scrim: #{dark[:shadow]};
--inverse-surface: #{dark[:inverse_surface]};
--on-inverse-surface: #{dark[:inverse_on_surface]};
--inverse-primary: #{dark[:inverse_primary]};
--primary-fixed: #{light[:primary_container]};
--on-primary-fixed: #{p[:primary][10]};
--primary-fixed-dim: #{p[:primary][80]};
--on-primary-fixed-variant: #{light[:on_primary_container]};
--secondary-fixed: #{light[:secondary_container]};
--on-secondary-fixed: #{p[:secondary][10]};
--secondary-fixed-dim: #{p[:secondary][80]};
--on-secondary-fixed-variant: #{light[:on_secondary_container]};
--tertiary-fixed: #{light[:tertiary_container]};
--on-tertiary-fixed: #{p[:tertiary][10]};
--tertiary-fixed-dim: #{p[:tertiary][80]};
--on-tertiary-fixed-variant: #{light[:on_tertiary_container]};
--surface-dim: #{dark[:background]};
--surface-bright: #{p[:neutral][24]};
--surface-container-lowest: #{p[:neutral][4]};
--surface-container-low: #{p[:neutral][10]};
--surface-container: #{p[:neutral][12]};
--surface-container-high: #{p[:neutral][17]};
--surface-container-highest: #{p[:neutral][22]};
}
@media (prefers-contrast: more) {
[data-color='#{hex_color.downcase}'][data-color-scheme='light'] {
--primary: #{p[:primary][10]};
--surface-tint: #{light[:primary]};
--on-primary: #{p[:neutral][99]};
--primary-container: #{light[:on_primary_container]};
--on-primary-container: #{p[:neutral][99]};
--secondary: #{p[:secondary][10]};
--on-secondary: #{p[:neutral][99]};
--secondary-container: #{light[:on_secondary_container]};
--on-secondary-container: #{p[:neutral][99]};
--on-secondary-container-transparent: #{p[:neutral][99]}14;
--tertiary: #{p[:tertiary][10]};
--on-tertiary: #{p[:neutral][99]};
--tertiary-container: #{light[:on_tertiary_container]};
--on-tertiary-container: #{p[:neutral][99]};
--error: #{p[:error][10]};
--on-error: #{p[:neutral][99]};
--error-container: #{p[:error][30]};
--on-error-container: #{p[:neutral][99]};
--background: #{light[:background]};
--on-background: #{light[:on_background]};
--surface: #{light[:surface]};
--on-surface: #{light[:on_background]};
--surface-variant: #{light[:surface_variant]};
--on-surface-variant: #{p[:neutral_variant][10]};
--outline: #{p[:neutral_variant][10]};
--outline-variant: #{p[:neutral_variant][10]};
--shadow: #{light[:shadow]};
--scrim: #{light[:shadow]};
--inverse-surface: #{light[:inverse_surface]};
--on-inverse-surface: #{p[:neutral][99]};
--inverse-primary: #{p[:primary][95]};
--primary-fixed: #{light[:on_primary_container]};
--on-primary-fixed: #{p[:neutral][99]};
--primary-fixed-dim: #{p[:primary][20]};
--on-primary-fixed-variant: #{p[:neutral][99]};
--secondary-fixed: #{light[:on_secondary_container]};
--on-secondary-fixed: #{p[:neutral][99]};
--secondary-fixed-dim: #{p[:secondary][20]};
--on-secondary-fixed-variant: #{p[:neutral][99]};
--tertiary-fixed: #{light[:on_tertiary_container]};
--on-tertiary-fixed: #{p[:neutral][99]};
--tertiary-fixed-dim: #{p[:tertiary][20]};
--on-tertiary-fixed-variant: #{p[:neutral][99]};
--surface-dim: #{p[:neutral][87]};
--surface-bright: #{light[:background]};
--surface-container-lowest: #{p[:neutral][100]};
--surface-container-low: #{p[:neutral][96]};
--surface-container: #{p[:neutral][94]};
--surface-container-high: #{p[:neutral][92]};
--surface-container-highest: #{p[:neutral][90]};
}
}
@media (prefers-contrast: more) {
[data-color='#{hex_color.downcase}'][data-color-scheme='dark'] {
--primary: #{p[:primary][95]};
--surface-tint: #{dark[:primary]};
--on-primary: #{p[:neutral][0]};
--primary-container: #{p[:primary][80]};
--on-primary-container: #{p[:neutral][0]};
--secondary: #{p[:primary][95]};
--on-secondary: #{p[:neutral][0]};
--secondary-container: #{p[:secondary][80]};
--on-secondary-container: #{p[:neutral][0]};
--on-secondary-container-transparent: #{p[:neutral][0]}14;
--tertiary: #{p[:neutral][95]};
--on-tertiary: #{p[:neutral][0]};
--tertiary-container: #{p[:tertiary][80]};
--on-tertiary-container: #{p[:neutral][0]};
--error: #{p[:neutral][95]};
--on-error: #{p[:neutral][0]};
--error-container: #{p[:error][80]};
--on-error-container: #{p[:neutral][0]};
--background: #{dark[:background]};
--on-background: #{dark[:on_background]};
--surface: #{dark[:surface]};
--on-surface: #{p[:neutral][100]};
--surface-variant: #{dark[:surface_variant]};
--on-surface-variant: #{p[:primary][95]};
--outline: #{p[:neutral_variant][80]};
--outline-variant: #{p[:neutral_variant][80]};
--shadow: #{dark[:shadow]};
--scrim: #{dark[:shadow]};
--inverse-surface: #{dark[:inverse_surface]};
--on-inverse-surface: #{p[:neutral][0]};
--inverse-primary: #{p[:primary][30]};
--primary-fixed: #{p[:primary][90]};
--on-primary-fixed: #{p[:neutral][0]};
--primary-fixed-dim: #{p[:primary][80]};
--on-primary-fixed-variant: #{p[:primary][10]};
--secondary-fixed: #{p[:secondary][90]};
--on-secondary-fixed: #{p[:neutral][0]};
--secondary-fixed-dim: #{p[:secondary][80]};
--on-secondary-fixed-variant: #{p[:secondary][10]};
--tertiary-fixed: #{p[:tertiary][90]};
--on-tertiary-fixed: #{p[:neutral][0]};
--tertiary-fixed-dim: #{p[:tertiary][80]};
--on-tertiary-fixed-variant: #{p[:tertiary][10]};
--surface-dim: #{dark[:background]};
--surface-bright: #{p[:neutral][24]};
--surface-container-lowest: #{p[:neutral][4]};
--surface-container-low: #{p[:neutral][10]};
--surface-container: #{p[:neutral][12]};
--surface-container-high: #{p[:neutral][17]};
--surface-container-highest: #{p[:neutral][22]};
}
}
@media (prefers-contrast: less) {
[data-color='#{hex_color.downcase}'][data-color-scheme='light'] {
--primary: #{light[:on_primary_container]};
--surface-tint: #{light[:primary]};
--on-primary: #{p[:neutral][100]};
--primary-container: #{p[:primary][60]};
--on-primary-container: #{p[:neutral][100]};
--secondary: #{light[:on_secondary_container]};
--on-secondary: #{p[:neutral][100]};
--secondary-container: #{p[:secondary][60]};
--on-secondary-container: #{p[:neutral][100]};
--on-secondary-container-transparent: #{p[:neutral][100]}14;
--tertiary: #{light[:on_tertiary_container]};
--on-tertiary: #{p[:neutral][100]};
--tertiary-container: #{p[:tertiary][60]};
--on-tertiary-container: #{p[:neutral][100]};
--error: #{p[:error][30]};
--on-error: #{p[:neutral][100]};
--error-container: #{p[:error][50]};
--on-error-container: #{p[:neutral][100]};
--background: #{light[:background]};
--on-background: #{light[:on_background]};
--surface: #{light[:surface]};
--on-surface: #{light[:on_background]};
--surface-variant: #{light[:surface_variant]};
--on-surface-variant: #{p[:neutral_variant][30]};
--outline: #{p[:neutral_variant][50]};
--outline-variant: #{p[:neutral_variant][70]};
--shadow: #{light[:shadow]};
--scrim: #{light[:shadow]};
--inverse-surface: #{light[:inverse_surface]};
--on-inverse-surface: #{p[:neutral][95]};
--inverse-primary: #{dark[:primary]};
--primary-fixed: #{p[:primary][60]};
--on-primary-fixed: #{p[:neutral][100]};
--primary-fixed-dim: #{p[:primary][50]};
--on-primary-fixed-variant: #{p[:neutral][100]};
--secondary-fixed: #{p[:secondary][60]};
--on-secondary-fixed: #{p[:neutral][100]};
--secondary-fixed-dim: #{p[:secondary][50]};
--on-secondary-fixed-variant: #{p[:neutral][100]};
--tertiary-fixed: #{p[:tertiary][60]};
--on-tertiary-fixed: #{p[:neutral][100]};
--tertiary-fixed-dim: #{p[:tertiary][50]};
--on-tertiary-fixed-variant: #{p[:neutral][100]};
--surface-dim: #{p[:neutral][87]};
--surface-bright: #{light[:background]};
--surface-container-lowest: #{p[:neutral][100]};
--surface-container-low: #{p[:neutral][96]};
--surface-container: #{p[:neutral][94]};
--surface-container-high: #{p[:neutral][92]};
--surface-container-highest: #{p[:neutral][90]};
}
}
@media (prefers-contrast: less) {
[data-color='#{hex_color.downcase}'][data-color-scheme='dark'] {
--primary: #{p[:primary][80]};
--surface-tint: #{dark[:primary]};
--on-primary: #{p[:primary][10]};
--primary-container: #{p[:primary][70]};
--on-primary-container: #{p[:neutral][0]};
--secondary: #{p[:secondary][80]};
--on-secondary: #{p[:secondary][10]};
--secondary-container: #{p[:secondary][70]};
--on-secondary-container: #{p[:neutral][0]};
--on-secondary-container-transparent: #{p[:neutral][0]}14;
--tertiary: #{p[:tertiary][80]};
--on-tertiary: #{p[:tertiary][10]};
--tertiary-container: #{p[:tertiary][70]};
--on-tertiary-container: #{p[:neutral][0]};
--error: #{p[:error][80]};
--on-error: #{p[:error][20]};
--error-container: #{p[:error][60]};
--on-error-container: #{p[:neutral][0]};
--background: #{dark[:background]};
--on-background: #{dark[:on_background]};
--surface: #{dark[:surface]};
--on-surface: #{p[:neutral][98]};
--surface-variant: #{dark[:surface_variant]};
--on-surface-variant: #{p[:neutral_variant][80]};
--outline: #{p[:neutral_variant][60]};
--outline-variant: #{p[:neutral_variant][30]};
--shadow: #{dark[:shadow]};
--scrim: #{dark[:shadow]};
--inverse-surface: #{dark[:inverse_surface]};
--on-inverse-surface: #{p[:neutral][20]};
--inverse-primary: #{p[:primary][40]};
--primary-fixed: #{light[:primary_container]};
--on-primary-fixed: #{p[:primary][10]};
--primary-fixed-dim: #{p[:primary][80]};
--on-primary-fixed-variant: #{p[:primary][30]};
--secondary-fixed: #{light[:secondary_container]};
--on-secondary-fixed: #{p[:secondary][10]};
--secondary-fixed-dim: #{p[:secondary][80]};
--on-secondary-fixed-variant: #{p[:secondary][30]};
--tertiary-fixed: #{light[:tertiary_container]};
--on-tertiary-fixed: #{p[:tertiary][10]};
--tertiary-fixed-dim: #{p[:tertiary][80]};
--on-tertiary-fixed-variant: #{p[:tertiary][30]};
--surface-dim: #{dark[:background]};
--surface-bright: #{p[:neutral][24]};
--surface-container-lowest: #{p[:neutral][4]};
--surface-container-low: #{p[:neutral][10]};
--surface-container: #{p[:neutral][12]};
--surface-container-high: #{p[:neutral][17]};
--surface-container-highest: #{p[:neutral][22]};
}
}
CSS
css
end
end
##
# Generate sample palettes when running directly
##
def generate_sample_palettes
puts "Generating sample Material Design 3 color palettes..."
puts
# Sample colors
colors = [
["6750A4", "Material You Purple"],
["1A73E8", "Google Blue"],
["E53935", "Material Red"],
["43A047", "Material Green"],
["FFA500", "Classic Orange"],
["9C27B0", "Material Purple"]
]
# Generate each palette
colors.each do |color, description|
puts "Generating palette for #{description} (#{color})..."
files = MaterialDesign3ColorGenerator.create_palette_files(color)
puts " Created: #{files.join(', ')}"
puts
end
puts "All sample palettes generated successfully!"
end
##
# Main entry point
##
if __FILE__ == $PROGRAM_NAME
if ARGV.empty? || ARGV[0] == '--samples'
# Generate sample palettes
if ARGV[0] == '--samples'
generate_sample_palettes
else
puts "Material Design 3 Color Generator"
puts "=================================="
puts
puts "Usage: ruby #{File.basename(__FILE__)} <hex_color> [options]"
puts
puts "Options:"
puts " --json Output JSON only"
puts " --css Output CSS only"
puts " --files Generate CSS and JSON files (default)"
puts " --samples Generate sample color palettes"
puts
puts "Examples:"
puts " ruby #{File.basename(__FILE__)} 6750A4"
puts " ruby #{File.basename(__FILE__)} 1A73E8 --json > blue.json"
puts " ruby #{File.basename(__FILE__)} --samples"
end
else
# Process a specific color
hex_color = ARGV[0].gsub(/^#/, '')
if ARGV.include?('--json')
# Output JSON to stdout
puts MaterialDesign3ColorGenerator.generate_json("##{hex_color}")
elsif ARGV.include?('--css')
# Output CSS to stdout
scheme = MaterialDesign3ColorGenerator.generate_material_scheme("##{hex_color}")
puts MaterialDesign3ColorGenerator.generate_css_content(hex_color, scheme)
else
# Generate files by default
MaterialDesign3ColorGenerator.create_palette_files(hex_color)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment