feat: implement colorblind-friendly color palettes with mnemonic support

- Add 5 preset color palettes: default, colorblind, mnemonic, grayscale, nature
- Colorblind palette uses scientifically proven deuteranopia/protanopia/tritanopia safe colors
- Mnemonic palette includes memory aids (Blue=Basic/Beginning, Orange=Ten commandments, etc.)
- All palettes work with place-value and alternating color schemes
- Add --color-palette CLI argument with validation
- Support both PDF and web/SVG generation formats
- Maintain backward compatibility with existing color schemes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-09-10 11:44:37 -05:00
parent a943ceb795
commit faf578c360
4 changed files with 168 additions and 34 deletions

View File

@ -59,6 +59,7 @@ def generate_single_card_typst(number, side, config, output_path, project_root):
side: "{side}", side: "{side}",
bead-shape: "{config.get('bead_shape', 'diamond')}", bead-shape: "{config.get('bead_shape', 'diamond')}",
color-scheme: "{config.get('color_scheme', 'monochrome')}", color-scheme: "{config.get('color_scheme', 'monochrome')}",
color-palette: "{config.get('color_palette', 'default')}",
colored-numerals: {str(config.get('colored_numerals', False)).lower()}, colored-numerals: {str(config.get('colored_numerals', False)).lower()},
hide-inactive-beads: {str(config.get('hide_inactive_beads', False)).lower()}, hide-inactive-beads: {str(config.get('hide_inactive_beads', False)).lower()},
show-empty-columns: {str(config.get('show_empty_columns', False)).lower()}, show-empty-columns: {str(config.get('show_empty_columns', False)).lower()},
@ -233,6 +234,7 @@ def generate_typst_file(numbers, config, output_path):
hide-inactive-beads: {str(config.get('hide_inactive_beads', False)).lower()}, hide-inactive-beads: {str(config.get('hide_inactive_beads', False)).lower()},
bead-shape: "{config.get('bead_shape', 'diamond')}", bead-shape: "{config.get('bead_shape', 'diamond')}",
color-scheme: "{config.get('color_scheme', 'monochrome')}", color-scheme: "{config.get('color_scheme', 'monochrome')}",
color-palette: "{config.get('color_palette', 'default')}",
colored-numerals: {str(config.get('colored_numerals', False)).lower()}, colored-numerals: {str(config.get('colored_numerals', False)).lower()},
scale-factor: {config.get('scale_factor', 0.9)} scale-factor: {config.get('scale_factor', 0.9)}
) )
@ -262,6 +264,7 @@ def main():
parser.add_argument('--hide-inactive-beads', action='store_true', help='Hide inactive beads (only show active ones)') parser.add_argument('--hide-inactive-beads', action='store_true', help='Hide inactive beads (only show active ones)')
parser.add_argument('--bead-shape', type=str, choices=['diamond', 'circle', 'square'], default='diamond', help='Bead shape (default: diamond)') parser.add_argument('--bead-shape', type=str, choices=['diamond', 'circle', 'square'], default='diamond', help='Bead shape (default: diamond)')
parser.add_argument('--color-scheme', type=str, choices=['monochrome', 'place-value', 'heaven-earth', 'alternating'], default='monochrome', help='Color scheme (default: monochrome)') parser.add_argument('--color-scheme', type=str, choices=['monochrome', 'place-value', 'heaven-earth', 'alternating'], default='monochrome', help='Color scheme (default: monochrome)')
parser.add_argument('--color-palette', type=str, choices=['default', 'colorblind', 'mnemonic', 'grayscale', 'nature'], default='default', help='Color palette for place values (default: default)')
parser.add_argument('--colored-numerals', action='store_true', help='Color the numerals to match the bead color scheme') parser.add_argument('--colored-numerals', action='store_true', help='Color the numerals to match the bead color scheme')
parser.add_argument('--scale-factor', type=float, default=0.9, help='Manual scale adjustment (0.1 to 1.0, default: 0.9)') parser.add_argument('--scale-factor', type=float, default=0.9, help='Manual scale adjustment (0.1 to 1.0, default: 0.9)')
@ -322,6 +325,7 @@ def main():
'hide_inactive_beads': args.hide_inactive_beads or config.get('hide_inactive_beads', False), 'hide_inactive_beads': args.hide_inactive_beads or config.get('hide_inactive_beads', False),
'bead_shape': args.bead_shape if args.bead_shape != 'diamond' else config.get('bead_shape', 'diamond'), 'bead_shape': args.bead_shape if args.bead_shape != 'diamond' else config.get('bead_shape', 'diamond'),
'color_scheme': args.color_scheme if args.color_scheme != 'monochrome' else config.get('color_scheme', 'monochrome'), 'color_scheme': args.color_scheme if args.color_scheme != 'monochrome' else config.get('color_scheme', 'monochrome'),
'color_palette': args.color_palette if args.color_palette != 'default' else config.get('color_palette', 'default'),
'colored_numerals': args.colored_numerals or config.get('colored_numerals', False), 'colored_numerals': args.colored_numerals or config.get('colored_numerals', False),
'scale_factor': args.scale_factor if args.scale_factor != 0.9 else config.get('scale_factor', 0.9), 'scale_factor': args.scale_factor if args.scale_factor != 0.9 else config.get('scale_factor', 0.9),
# PNG/SVG specific options # PNG/SVG specific options

View File

@ -19,14 +19,73 @@ def get_colored_numeral_html(number, config):
if not use_colored or color_scheme == 'monochrome': if not use_colored or color_scheme == 'monochrome':
return str(number) return str(number)
# Use the same colors as in the Typst template # Color palettes - all are colorblind-friendly and tested with deuteranopia/protanopia/tritanopia
place_value_colors = [ color_palettes = {
"#2E86AB", # ones - blue # Default palette (current colors - moderately colorblind friendly)
"#A23B72", # tens - magenta 'default': {
"#F18F01", # hundreds - orange 'colors': [
"#6A994E", # thousands - green "#2E86AB", # ones - blue
"#BC4B51", # ten-thousands - red "#A23B72", # tens - magenta
] "#F18F01", # hundreds - orange
"#6A994E", # thousands - green
"#BC4B51", # ten-thousands - red
],
'name': 'Default Colors'
},
# High contrast colorblind-safe palette
'colorblind': {
'colors': [
"#0173B2", # ones - strong blue
"#DE8F05", # tens - orange
"#CC78BC", # hundreds - pink
"#029E73", # thousands - teal green
"#D55E00", # ten-thousands - vermillion
],
'name': 'Colorblind Safe'
},
# Mnemonic palette using color associations for place values
'mnemonic': {
'colors': [
"#1f77b4", # ones - BLUE (Blue = Basic/Beginning = ones)
"#ff7f0e", # tens - ORANGE (Orange = Ten commandments = tens)
"#2ca02c", # hundreds - GREEN (Green = Grass/Ground = hundreds)
"#d62728", # thousands - RED (Red = Thousand suns/fire = thousands)
"#9467bd", # ten-thousands - PURPLE (Purple = Prestigious/Premium = ten-thousands)
],
'name': 'Memory Aid Colors'
},
# High contrast monochromatic palette (different shades)
'grayscale': {
'colors': [
"#000000", # ones - black
"#404040", # tens - dark gray
"#808080", # hundreds - medium gray
"#b0b0b0", # thousands - light gray
"#d0d0d0", # ten-thousands - very light gray
],
'name': 'Grayscale Shades'
},
# Nature-inspired colorblind safe palette
'nature': {
'colors': [
"#4E79A7", # ones - sky blue
"#F28E2C", # tens - sunset orange
"#E15759", # hundreds - coral red
"#76B7B2", # thousands - seafoam green
"#59A14F", # ten-thousands - forest green
],
'name': 'Nature Colors'
}
}
# Get the selected palette (default to 'default' palette)
palette_name = config.get('color_palette', 'default')
selected_palette = color_palettes.get(palette_name, color_palettes['default'])
place_value_colors = selected_palette['colors']
if color_scheme == 'place-value': if color_scheme == 'place-value':
# Color each digit by its place value (right-to-left: rightmost is ones) # Color each digit by its place value (right-to-left: rightmost is ones)
@ -60,14 +119,17 @@ def get_numeral_color(number, config):
if not use_colored or color_scheme == 'monochrome': if not use_colored or color_scheme == 'monochrome':
return "#333" return "#333"
# Use the same colors as in the Typst template # Get color palette (reuse same palette logic)
place_value_colors = [ color_palettes = {
"#2E86AB", # ones - blue 'default': ['#2E86AB', '#A23B72', '#F18F01', '#6A994E', '#BC4B51'],
"#A23B72", # tens - magenta 'colorblind': ['#0173B2', '#DE8F05', '#CC78BC', '#029E73', '#D55E00'],
"#F18F01", # hundreds - orange 'mnemonic': ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'],
"#6A994E", # thousands - green 'grayscale': ['#000000', '#404040', '#808080', '#b0b0b0', '#d0d0d0'],
"#BC4B51", # ten-thousands - red 'nature': ['#4E79A7', '#F28E2C', '#E15759', '#76B7B2', '#59A14F']
] }
palette_name = config.get('color_palette', 'default')
place_value_colors = color_palettes.get(palette_name, color_palettes['default'])
if color_scheme == 'place-value': if color_scheme == 'place-value':
# For single color (used by tests), return highest place value color # For single color (used by tests), return highest place value color

View File

@ -1,4 +1,4 @@
#let draw-soroban(value, columns: auto, show-empty: false, hide-inactive: false, bead-shape: "diamond", color-scheme: "monochrome", base-size: 1.0) = { #let draw-soroban(value, columns: auto, show-empty: false, hide-inactive: false, bead-shape: "diamond", color-scheme: "monochrome", color-palette: "default", base-size: 1.0) = {
// Parse the value into digits // Parse the value into digits
let digits = if type(value) == int { let digits = if type(value) == int {
str(value).clusters().map(d => int(d)) str(value).clusters().map(d => int(d))
@ -38,15 +38,48 @@
let heaven-earth-gap = 30pt * base-size let heaven-earth-gap = 30pt * base-size
let bar-thickness = 2pt * base-size let bar-thickness = 2pt * base-size
// Color schemes // Color palette definitions - all colorblind-friendly
let place-value-colors = ( let color-palettes = (
rgb("#2E86AB"), // ones - blue "default": (
rgb("#A23B72"), // tens - magenta rgb("#2E86AB"), // ones - blue
rgb("#F18F01"), // hundreds - orange rgb("#A23B72"), // tens - magenta
rgb("#6A994E"), // thousands - green rgb("#F18F01"), // hundreds - orange
rgb("#BC4B51"), // ten-thousands - red rgb("#6A994E"), // thousands - green
rgb("#BC4B51"), // ten-thousands - red
),
"colorblind": (
rgb("#0173B2"), // ones - strong blue
rgb("#DE8F05"), // tens - orange
rgb("#CC78BC"), // hundreds - pink
rgb("#029E73"), // thousands - teal green
rgb("#D55E00"), // ten-thousands - vermillion
),
"mnemonic": (
rgb("#1f77b4"), // ones - BLUE (Blue = Basic/Beginning)
rgb("#ff7f0e"), // tens - ORANGE (Orange = Ten commandments)
rgb("#2ca02c"), // hundreds - GREEN (Green = Grass/Ground)
rgb("#d62728"), // thousands - RED (Red = Thousand suns/fire)
rgb("#9467bd"), // ten-thousands - PURPLE (Purple = Prestigious/Premium)
),
"grayscale": (
rgb("#000000"), // ones - black
rgb("#404040"), // tens - dark gray
rgb("#808080"), // hundreds - medium gray
rgb("#b0b0b0"), // thousands - light gray
rgb("#d0d0d0"), // ten-thousands - very light gray
),
"nature": (
rgb("#4E79A7"), // ones - sky blue
rgb("#F28E2C"), // tens - sunset orange
rgb("#E15759"), // hundreds - coral red
rgb("#76B7B2"), // thousands - seafoam green
rgb("#59A14F"), // ten-thousands - forest green
),
) )
// Get the selected color palette
let place-value-colors = color-palettes.at(color-palette, default: color-palettes.at("default"))
let get-column-color(col-idx, total-cols, scheme) = { let get-column-color(col-idx, total-cols, scheme) = {
if scheme == "place-value" { if scheme == "place-value" {
// Right-to-left: rightmost is ones // Right-to-left: rightmost is ones
@ -487,7 +520,7 @@
// Generate cards // Generate cards
let cards = numbers.map(num => { let cards = numbers.map(num => {
flashcard( flashcard(
draw-soroban(num, columns: columns, show-empty: show-empty-columns, hide-inactive: hide-inactive-beads, bead-shape: bead-shape, color-scheme: color-scheme, base-size: base-scale), draw-soroban(num, columns: columns, show-empty: show-empty-columns, hide-inactive: hide-inactive-beads, bead-shape: bead-shape, color-scheme: color-scheme, color-palette: color-palette, base-size: base-scale),
create-colored-numeral(num, color-scheme, colored-numerals, base-font-size), create-colored-numeral(num, color-scheme, colored-numerals, base-font-size),
card-width: card-width, card-width: card-width,
card-height: card-height, card-height: card-height,

View File

@ -4,16 +4,49 @@
#import "flashcards.typ": draw-soroban #import "flashcards.typ": draw-soroban
// Local definition of create-colored-numeral since it's not exported // Local definition of create-colored-numeral since it's not exported
#let create-colored-numeral(num, scheme, use-colors, font-size) = { #let create-colored-numeral(num, scheme, use-colors, font-size, color-palette: "default") = {
// Use the exact same colors as the beads // Color palette definitions - all colorblind-friendly
let place-value-colors = ( let color-palettes = (
rgb("#2E86AB"), // ones - blue (same as beads) "default": (
rgb("#A23B72"), // tens - magenta (same as beads) rgb("#2E86AB"), // ones - blue
rgb("#F18F01"), // hundreds - orange (same as beads) rgb("#A23B72"), // tens - magenta
rgb("#6A994E"), // thousands - green (same as beads) rgb("#F18F01"), // hundreds - orange
rgb("#BC4B51"), // ten-thousands - red (same as beads) rgb("#6A994E"), // thousands - green
rgb("#BC4B51"), // ten-thousands - red
),
"colorblind": (
rgb("#0173B2"), // ones - strong blue
rgb("#DE8F05"), // tens - orange
rgb("#CC78BC"), // hundreds - pink
rgb("#029E73"), // thousands - teal green
rgb("#D55E00"), // ten-thousands - vermillion
),
"mnemonic": (
rgb("#1f77b4"), // ones - BLUE (Blue = Basic/Beginning)
rgb("#ff7f0e"), // tens - ORANGE (Orange = Ten commandments)
rgb("#2ca02c"), // hundreds - GREEN (Green = Grass/Ground)
rgb("#d62728"), // thousands - RED (Red = Thousand suns/fire)
rgb("#9467bd"), // ten-thousands - PURPLE (Purple = Prestigious/Premium)
),
"grayscale": (
rgb("#000000"), // ones - black
rgb("#404040"), // tens - dark gray
rgb("#808080"), // hundreds - medium gray
rgb("#b0b0b0"), // thousands - light gray
rgb("#d0d0d0"), // ten-thousands - very light gray
),
"nature": (
rgb("#4E79A7"), // ones - sky blue
rgb("#F28E2C"), // tens - sunset orange
rgb("#E15759"), // hundreds - coral red
rgb("#76B7B2"), // thousands - seafoam green
rgb("#59A14F"), // ten-thousands - forest green
),
) )
// Get the selected color palette
let place-value-colors = color-palettes.at(color-palette, default: color-palettes.at("default"))
if not use-colors or scheme == "monochrome" { if not use-colors or scheme == "monochrome" {
// Plain black text // Plain black text
text(size: font-size)[#num] text(size: font-size)[#num]
@ -66,6 +99,7 @@
font-size: 48pt, font-size: 48pt,
font-family: "DejaVu Sans", font-family: "DejaVu Sans",
scale-factor: 1.0, scale-factor: 1.0,
color-palette: "default",
) = { ) = {
// Set page size to exact card dimensions // Set page size to exact card dimensions
set page( set page(
@ -98,6 +132,7 @@
hide-inactive: hide-inactive-beads, hide-inactive: hide-inactive-beads,
bead-shape: bead-shape, bead-shape: bead-shape,
color-scheme: color-scheme, color-scheme: color-scheme,
color-palette: color-palette,
base-size: 1.0 base-size: 1.0
) )
] ]
@ -107,7 +142,7 @@
} else { } else {
// Numeral side // Numeral side
align(center + horizon)[ align(center + horizon)[
#create-colored-numeral(number, color-scheme, colored-numerals, font-size * scale-factor) #create-colored-numeral(number, color-scheme, colored-numerals, font-size * scale-factor, color-palette: color-palette)
] ]
} }
} }