Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pipeline/adapters/animation/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand All @@ -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
Expand Down
102 changes: 101 additions & 1 deletion pipeline/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion scenes/two_pointers/scene.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
Expand Down
60 changes: 39 additions & 21 deletions templates/array_animation.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<style>
:root {
--bg: {{ theme.background }};
--text: {{ theme.text }};
--title: {{ theme.title }};
--target: {{ theme.target }};
--cell-bg: {{ theme.cell_bg }};
--cell-border: {{ theme.cell_border }};
--cell-text: {{ theme.cell_text }};
--pointer-left: {{ theme.pointer_left }};
--pointer-right: {{ theme.pointer_right }};
--highlight-found: {{ theme.highlight_found }};
--highlight-sum: {{ theme.highlight_sum }};
--message: {{ theme.message }};
--index-label: {{ theme.index_label }};
--step-indicator: {{ theme.step_indicator }};
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #1a1a2e;
color: #eee;
background: var(--bg);
color: var(--text);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
min-height: 100vh;
display: flex;
Expand All @@ -35,13 +52,13 @@
.title {
font-size: 28px;
font-weight: 600;
color: #e94560;
color: var(--title);
text-align: center;
}

.target-display {
font-size: 20px;
color: #16c79a;
color: var(--target);
text-align: center;
}

Expand All @@ -62,47 +79,48 @@
justify-content: center;
font-size: 28px;
font-weight: 600;
background: #0f3460;
border: 3px solid #16c79a;
background: var(--cell-bg);
border: 3px solid var(--cell-border);
border-radius: 12px;
position: relative;
transition: all 0.3s ease;
color: var(--cell-text);
}

.array-cell.highlight-left {
background: #e94560;
border-color: #e94560;
background: var(--pointer-left);
border-color: var(--pointer-left);
transform: scale(1.1);
}

.array-cell.highlight-right {
background: #00b4d8;
border-color: #00b4d8;
background: var(--pointer-right);
border-color: var(--pointer-right);
transform: scale(1.1);
}

.array-cell.highlight-both {
background: linear-gradient(135deg, #e94560 50%, #00b4d8 50%);
border-color: #f7d716;
background: linear-gradient(135deg, var(--pointer-left) 50%, var(--pointer-right) 50%);
border-color: var(--highlight-sum);
transform: scale(1.15);
}

.array-cell.highlight-sum {
box-shadow: 0 0 20px rgba(247, 215, 22, 0.6);
box-shadow: 0 0 20px color-mix(in srgb, var(--highlight-sum) 60%, transparent);
}

.array-cell.found {
background: #16c79a;
border-color: #16c79a;
background: var(--highlight-found);
border-color: var(--highlight-found);
transform: scale(1.2);
box-shadow: 0 0 30px rgba(22, 199, 154, 0.8);
box-shadow: 0 0 30px color-mix(in srgb, var(--highlight-found) 80%, transparent);
}

.index-label {
position: absolute;
bottom: -28px;
font-size: 14px;
color: #888;
color: var(--index-label);
}

.pointer {
Expand All @@ -117,12 +135,12 @@
}

.pointer.left-pointer {
background: #e94560;
background: var(--pointer-left);
color: white;
}

.pointer.right-pointer {
background: #00b4d8;
background: var(--pointer-right);
color: white;
}

Expand All @@ -132,15 +150,15 @@

.message-display {
font-size: 24px;
color: #f7d716;
color: var(--message);
text-align: center;
min-height: 36px;
transition: opacity 0.3s ease;
}

.step-indicator {
font-size: 14px;
color: #666;
color: var(--step-indicator);
}

/* Animation keyframes for pointer movement */
Expand Down
6 changes: 3 additions & 3 deletions tests/fixtures/sample_scene.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"type": "array_pointers",
"config": {
"array": [2, 7, 11, 15],
"target": 9,
"theme": "dark"
}
"target": 9
},
"theme": {"preset": "dark"}
},
"steps": [
{
Expand Down
63 changes: 61 additions & 2 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
import pytest
from pydantic import ValidationError

from pipeline.schema import SceneSpec, Step, StepState, VisualizationConfig
from pipeline.schema import (
PRESET_THEMES,
SceneSpec,
Step,
StepState,
ThemeColors,
ThemeConfig,
VisualizationConfig,
)


class TestStepState:
Expand Down Expand Up @@ -101,6 +109,57 @@ def test_with_config(self):
assert viz.config["array"] == [1, 2, 3]
assert viz.config["target"] == 5

def test_theme_defaults_to_dark(self):
"""Theme should default to dark preset."""
viz = VisualizationConfig(type="array_pointers")
assert viz.theme.preset == "dark"


class TestThemeConfig:
"""Tests for ThemeConfig model."""

def test_default_preset(self):
"""ThemeConfig should default to dark preset."""
theme = ThemeConfig()
assert theme.preset == "dark"

def test_resolve_colors_dark(self):
"""Dark preset should resolve to expected colors."""
theme = ThemeConfig(preset="dark")
colors = theme.resolve_colors()
assert colors["background"] == "#1a1a2e"
assert colors["pointer_left"] == "#e94560"

def test_resolve_colors_light(self):
"""Light preset should resolve to expected colors."""
theme = ThemeConfig(preset="light")
colors = theme.resolve_colors()
assert colors["background"] == "#f5f5f5"
assert colors["text"] == "#333"

def test_resolve_colors_neetcode(self):
"""Neetcode preset should resolve to expected colors."""
theme = ThemeConfig(preset="neetcode")
colors = theme.resolve_colors()
assert colors["background"] == "#0a0a0f"

def test_color_overrides(self):
"""Custom colors should override preset values."""
theme = ThemeConfig(
preset="dark",
colors=ThemeColors(background="#000000", title="#ff0000")
)
colors = theme.resolve_colors()
assert colors["background"] == "#000000"
assert colors["title"] == "#ff0000"
# Non-overridden values should use preset
assert colors["text"] == "#eee"

def test_invalid_preset_rejected(self):
"""Invalid preset should raise ValidationError."""
with pytest.raises(ValidationError):
ThemeConfig(preset="invalid_theme")


class TestSceneSpec:
"""Tests for SceneSpec model."""
Expand Down Expand Up @@ -195,7 +254,7 @@ def test_sample_scene_visualization(self, sample_scene_data: dict):
assert spec.visualization.type == "array_pointers"
assert spec.visualization.config["array"] == [2, 7, 11, 15]
assert spec.visualization.config["target"] == 9
assert spec.visualization.config["theme"] == "dark"
assert spec.visualization.theme.preset == "dark"

def test_sample_scene_steps(self, sample_scene_data: dict):
"""Sample scene should have expected steps."""
Expand Down