diff --git a/pipeline/adapters/animation/html.py b/pipeline/adapters/animation/html.py index c55fce2..f47117a 100644 --- a/pipeline/adapters/animation/html.py +++ b/pipeline/adapters/animation/html.py @@ -89,6 +89,9 @@ def render(self, spec: SceneSpec, timing: List[float], output_path: Path) -> Pat array = viz_config.get("array", []) target = viz_config.get("target", 0) + # Resolve theme colors + theme = spec.visualization.get_resolved_theme() + # Convert steps to serializable format steps_data = [ { @@ -111,6 +114,7 @@ def render(self, spec: SceneSpec, timing: List[float], output_path: Path) -> Pat target=target, steps=steps_data, timing=timing, + theme=theme, ) # Ensure output directory exists diff --git a/pipeline/schema.py b/pipeline/schema.py index 550ba0f..cc4996d 100644 --- a/pipeline/schema.py +++ b/pipeline/schema.py @@ -4,10 +4,104 @@ Defines the data models for scene specifications that drive the animation pipeline. """ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, Field +# Preset theme definitions +PRESET_THEMES = { + "dark": { + "background": "#1a1a2e", + "text": "#eee", + "title": "#e94560", + "target": "#16c79a", + "cell_bg": "#0f3460", + "cell_border": "#16c79a", + "cell_text": "#eee", + "pointer_left": "#e94560", + "pointer_right": "#00b4d8", + "highlight_found": "#16c79a", + "highlight_sum": "#f7d716", + "message": "#f7d716", + "index_label": "#888", + "step_indicator": "#666", + }, + "light": { + "background": "#f5f5f5", + "text": "#333", + "title": "#d63384", + "target": "#198754", + "cell_bg": "#ffffff", + "cell_border": "#198754", + "cell_text": "#333", + "pointer_left": "#d63384", + "pointer_right": "#0d6efd", + "highlight_found": "#198754", + "highlight_sum": "#ffc107", + "message": "#fd7e14", + "index_label": "#6c757d", + "step_indicator": "#adb5bd", + }, + "neetcode": { + "background": "#0a0a0f", + "text": "#e5e5e5", + "title": "#ff6b6b", + "target": "#51cf66", + "cell_bg": "#1a1a2e", + "cell_border": "#4dabf7", + "cell_text": "#e5e5e5", + "pointer_left": "#ff6b6b", + "pointer_right": "#4dabf7", + "highlight_found": "#51cf66", + "highlight_sum": "#ffd43b", + "message": "#ffd43b", + "index_label": "#868e96", + "step_indicator": "#495057", + }, +} + + +class ThemeColors(BaseModel): + """Custom color overrides for theming. + + All colors are optional. Unspecified colors fall back to the base preset. + """ + background: Optional[str] = None + text: Optional[str] = None + title: Optional[str] = None + target: Optional[str] = None + cell_bg: Optional[str] = None + cell_border: Optional[str] = None + cell_text: Optional[str] = None + pointer_left: Optional[str] = None + pointer_right: Optional[str] = None + highlight_found: Optional[str] = None + highlight_sum: Optional[str] = None + message: Optional[str] = None + index_label: Optional[str] = None + step_indicator: Optional[str] = None + + +class ThemeConfig(BaseModel): + """Theme configuration for visualization colors. + + Attributes: + preset: Base preset theme ("dark", "light", "neetcode") + colors: Custom color overrides + """ + preset: Literal["dark", "light", "neetcode"] = "dark" + colors: Optional[ThemeColors] = None + + def resolve_colors(self) -> Dict[str, str]: + """Resolve final colors by merging preset with overrides.""" + base = PRESET_THEMES[self.preset].copy() + if self.colors: + for key, value in self.colors.model_dump().items(): + if value is not None: + base[key] = value + return base + + class StepState(BaseModel): """Visualization state for a single step. @@ -42,9 +136,15 @@ class VisualizationConfig(BaseModel): Attributes: type: Type of visualization (e.g., "array_pointers") config: Type-specific configuration dictionary + theme: Optional theme configuration """ type: str config: Dict[str, Any] = Field(default_factory=dict) + theme: ThemeConfig = Field(default_factory=ThemeConfig) + + def get_resolved_theme(self) -> Dict[str, str]: + """Get fully resolved theme colors.""" + return self.theme.resolve_colors() class SceneSpec(BaseModel): diff --git a/scenes/two_pointers/scene.json b/scenes/two_pointers/scene.json index fbb2116..76401e9 100644 --- a/scenes/two_pointers/scene.json +++ b/scenes/two_pointers/scene.json @@ -4,7 +4,8 @@ "description": "Two pointer technique on sorted array", "visualization": { "type": "array_pointers", - "config": {"array": [2, 7, 11, 15], "target": 9, "theme": "dark"} + "config": {"array": [2, 7, 11, 15], "target": 9}, + "theme": {"preset": "dark"} }, "steps": [ {"id": "init", "narration": "The two pointer approach starts with pointers at both ends. Left at two, right at fifteen.", "state": {"left": 0, "right": 3, "highlight": null, "message": "Initialize: L=0, R=3"}}, diff --git a/templates/array_animation.html b/templates/array_animation.html index 736ab1a..f79d4c6 100644 --- a/templates/array_animation.html +++ b/templates/array_animation.html @@ -5,6 +5,23 @@