Created
May 13, 2025 18:46
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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