Skip to content
Merged
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
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ pixel explosions and a spinning radar. Runs with GitHub Actions.
**Lincoln — default carrier**

<picture>
<source media="(prefers-reduced-motion: reduce)" srcset="examples/demo/git-jet.png?v=runway-queue-20261102">
<img src="examples/demo/git-jet.gif?v=runway-queue-20261102" width="960" alt="A pixel jet launches from a Lincoln-inspired carrier through rolling ocean water, with an angled runway, detailed island and spinning radar.">
<source media="(prefers-reduced-motion: reduce)" srcset="examples/demo/git-jet.png?v=graph-theme-20261103">
<img src="examples/demo/git-jet.gif?v=graph-theme-20261103" width="960" alt="A pixel jet launches from a Lincoln-inspired carrier through rolling ocean water, with an angled runway, detailed island and spinning radar.">
</picture>

**Classic — original carrier**
**Classic — original carrier with the green graph**

<picture>
<source media="(prefers-reduced-motion: reduce)" srcset="examples/demo-classic/git-jet.png?v=runway-queue-20261102">
<img src="examples/demo-classic/git-jet.gif?v=runway-queue-20261102" width="960" alt="The original ivory-island carrier with a spinning radar and flowing ocean wake launches a pixel jet to clear the commit calendar.">
<source media="(prefers-reduced-motion: reduce)" srcset="examples/demo-classic/git-jet.png?v=graph-theme-20261103">
<img src="examples/demo-classic/git-jet.gif?v=graph-theme-20261103" width="960" alt="The original ivory-island carrier with a spinning radar and flowing ocean wake launches a pixel jet to clear a green commit calendar.">
</picture>

Choose `carrier: 'lincoln'` (default) or `carrier: 'classic'` in your workflow.
Expand All @@ -26,6 +26,11 @@ Choose `water: 'ocean'` (default) for the flowing ocean surface and wake, or
`water: 'original'` for the original small wave patches. This works with either
carrier. For the CLI, use `--water ocean` or `--water original`.

Choose `graph-theme: 'github'` for GitHub green activity cells, as shown
in the Classic demo. The default is `graph-theme: 'blue'`, shown in the Lincoln
demo. Either graph theme works with either carrier and water style.
For the CLI, use `--graph-theme github` or `--graph-theme blue`.

Both demos show **three jets** and **56 synthetic commits across 14 active days**. Live mode uses your
actual data. **One attack clears one day cell**, regardless of that day's count.
The jet returns to land, the calendar rebuilds, and the animation loops.
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ inputs:
water:
description: Water artwork, ocean (default) or original
default: ocean
graph-theme:
description: Calendar colors, blue (default) or github for GitHub green
default: blue
auto-compress:
description: Automatically lower scale then FPS to fit GIF limits without skipping day cells
default: 'true'
Expand Down Expand Up @@ -72,6 +75,7 @@ runs:
JET_JETS: ${{ inputs.jets }}
JET_CARRIER: ${{ inputs.carrier }}
JET_WATER: ${{ inputs.water }}
JET_GRAPH_THEME: ${{ inputs.graph-theme }}
JET_AUTO_COMPRESS: ${{ inputs.auto-compress }}
JET_DEMO: ${{ inputs.demo }}
JET_SEED: ${{ inputs.seed }}
Expand Down
Binary file modified examples/demo-classic/git-jet.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/demo-classic/git-jet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion examples/demo-classic/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"jets": 3,
"carrier": "classic",
"water": "ocean",
"graph_theme": "github",
"seed": "git-jet-loop",
"flights": [
{
Expand Down Expand Up @@ -63,7 +64,7 @@
"duration_seconds": 32.0,
"transparent": true,
"decoded_frames_match": true,
"gif_bytes": 16526574,
"gif_bytes": 16553060,
"targets": [
{
"date": "2026-10-18",
Expand Down
Binary file modified examples/demo/git-jet.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/demo/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"jets": 3,
"carrier": "lincoln",
"water": "ocean",
"graph_theme": "blue",
"seed": "git-jet-loop",
"flights": [
{
Expand Down
1 change: 1 addition & 0 deletions examples/profile-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
days: '365'
carrier: lincoln # Choose lincoln (default) or classic.
water: ocean # Choose ocean (default) or original for the old water.
graph-theme: blue # Choose blue (default) or github for green activity cells.
jets: '1' # Choose 1, 2 or 3; more jets shorten busy calendars.
- name: Publish README images
env:
Expand Down
6 changes: 5 additions & 1 deletion scripts/render_demos.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
def main():
root = Path(__file__).resolve().parents[1]
calendar = Calendar.read(root / "examples/demo/calendar.json")
for carrier, folder in (("lincoln", "demo"), ("classic", "demo-classic")):
for carrier, folder, graph_theme in (
("lincoln", "demo", "blue"),
("classic", "demo-classic", "github"),
):
report = render_assets(
calendar,
root / "examples" / folder,
Options(),
jets=3,
carrier=carrier,
graph_theme=graph_theme,
seed="git-jet-loop",
source="demo",
)
Expand Down
2 changes: 2 additions & 0 deletions scripts/run_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def run():
os.environ.get("JET_CARRIER", "lincoln"),
"--water",
os.environ.get("JET_WATER", "ocean"),
"--graph-theme",
os.environ.get("JET_GRAPH_THEME", "blue"),
]
if demo == "true":
args += ["--demo", "--days", os.environ.get("JET_DAYS", "365")]
Expand Down
19 changes: 18 additions & 1 deletion src/git_jet/art.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@
DEEP = "#39799c"
FOAM = "#f1f4e8"
GRID = ("#244252", "#28556d", "#39799c", "#70b3ce", "#bfe6eb")
GITHUB_GRID = ("#30363d", "#0e4429", "#006d32", "#26a641", "#39d353")
DEFAULT_GRAPH_THEME = "blue"
GRAPH_THEMES = ("blue", "github")
DEFAULT_CARRIER = "lincoln"
CARRIERS = ("lincoln", "classic")


def validate_graph_theme(graph_theme):
if not isinstance(graph_theme, str) or graph_theme not in GRAPH_THEMES:
raise ValueError("graph theme must be blue or github")
return graph_theme


def validate_carrier(carrier):
if not isinstance(carrier, str) or carrier not in CARRIERS:
raise ValueError("carrier must be lincoln or classic")
Expand Down Expand Up @@ -340,10 +349,18 @@ def block(u, v, w, depth, z, height):
return image


def commit_tile(level, destroyed=False):
def commit_tile(level, destroyed=False, *, theme=DEFAULT_GRAPH_THEME):
validate_graph_theme(theme)
image = Image.new("RGBA", (7, 7))
draw = ImageDraw.Draw(image)
outline = [(1, 0), (5, 0), (6, 1), (6, 5), (5, 6), (1, 6), (0, 5), (0, 1)]
if theme == "github":
# Flat green activity squares with neutral, transparent empty cells.
active = level and not destroyed
draw.polygon(outline, fill=GITHUB_GRID[min(4, level)] if active else GITHUB_GRID[0])
if not active:
draw.rectangle((1, 1, 5, 5), fill=(0, 0, 0, 0))
return image
draw.polygon(outline, fill=GRID[0])
if level and not destroyed:
draw.rectangle((1, 1, 5, 5), fill=GRID[min(4, level)])
Expand Down
9 changes: 8 additions & 1 deletion src/git_jet/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from datetime import date
from pathlib import Path

from .art import CARRIERS, DEFAULT_CARRIER
from .art import CARRIERS, DEFAULT_CARRIER, DEFAULT_GRAPH_THEME, GRAPH_THEMES
from .calendar import Calendar
from .demo import demo_calendar
from .export import Options, render_assets
Expand Down Expand Up @@ -44,6 +44,12 @@ def main(argv=None):
default=DEFAULT_WATER,
help="water artwork: ocean or original (default: ocean)",
)
render.add_argument(
"--graph-theme",
choices=GRAPH_THEMES,
default=DEFAULT_GRAPH_THEME,
help="calendar colors: blue (default) or github (green)",
)
render.add_argument(
"--jets", type=int, choices=(1, 2, 3), default=1, help="simultaneous jets (default: 1)"
)
Expand Down Expand Up @@ -86,6 +92,7 @@ def main(argv=None):
jets=args.jets,
carrier=args.carrier,
water=args.water,
graph_theme=args.graph_theme,
seed=args.seed,
source=source_name,
auto_compress=args.auto_compress,
Expand Down
12 changes: 10 additions & 2 deletions src/git_jet/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __post_init__(self):
def palette():
samples = [
*(art.carrier_sprite(carrier=carrier) for carrier in art.CARRIERS),
*(art.commit_tile(i) for i in range(5)),
*(art.commit_tile(i, theme=theme) for theme in art.GRAPH_THEMES for i in range(5)),
*(art.explosion_sprite(i / 10) for i in range(10)),
*(art.jet_sprite(0, thrust, bank) for thrust in range(3) for bank in (-1, 0, 1)),
]
Expand Down Expand Up @@ -139,6 +139,7 @@ def export(
"jets": renderer.jet_count,
"carrier": renderer.carrier,
"water": renderer.water,
"graph_theme": renderer.graph_theme,
"seed": renderer.flight.seed,
"flights": [
{
Expand Down Expand Up @@ -190,6 +191,7 @@ def render_assets(
jets=1,
carrier=art.DEFAULT_CARRIER,
water=DEFAULT_WATER,
graph_theme=art.DEFAULT_GRAPH_THEME,
seed=None,
source="live",
auto_compress=True,
Expand All @@ -203,7 +205,13 @@ def render_assets(
for fps in rates:
try:
renderer = Renderer(
calendar, fps=fps, seed=seed, jets=jets, carrier=carrier, water=water
calendar,
fps=fps,
seed=seed,
jets=jets,
carrier=carrier,
water=water,
graph_theme=graph_theme,
)
scales = (
range(options.scale, 0, -1)
Expand Down
6 changes: 5 additions & 1 deletion src/git_jet/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from .art import (
CREAM,
DEFAULT_CARRIER,
DEFAULT_GRAPH_THEME,
GOLD,
carrier_sprite,
commit_tile,
explosion_sprite,
jet_sprite,
validate_carrier,
validate_graph_theme,
)
from .calendar import Calendar
from .flight import (
Expand All @@ -39,12 +41,14 @@ def __init__(
jets=1,
carrier=DEFAULT_CARRIER,
water=DEFAULT_WATER,
graph_theme=DEFAULT_GRAPH_THEME,
):
if type(jets) is not int or not 1 <= jets <= 3:
raise ValueError("jets must be an integer in 1..3")
self.jet_count = jets
self.carrier = validate_carrier(carrier)
self.water = validate_water(water)
self.graph_theme = validate_graph_theme(graph_theme)
self.calendar = calendar
self.cells = calendar.cells()
self.flight = (
Expand Down Expand Up @@ -74,7 +78,7 @@ def grid_frame(self, frame_number: int):
hit = self.hit_by_date.get(cell.day.date) if cell.day else None
cleared = hit is not None and frame >= hit and not self.flight.restored(cell, frame)
image.alpha_composite(
commit_tile(level, cleared),
commit_tile(level, cleared, theme=self.graph_theme),
(GRID_X + cell.column * GRID_STEP, GRID_Y + cell.row * GRID_STEP),
)
return image
Expand Down
4 changes: 4 additions & 0 deletions tests/test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def test_action_forwards_inputs_and_writes_only_successful_output(monkeypatch, t
monkeypatch.setenv("JET_JETS", "3")
monkeypatch.setenv("JET_CARRIER", "classic")
monkeypatch.setenv("JET_WATER", "original")
monkeypatch.setenv("JET_GRAPH_THEME", "github")
monkeypatch.setenv("JET_AUTO_COMPRESS", "false")
monkeypatch.setenv("JET_OUTPUT_DIR", "generated")
monkeypatch.setenv("JET_SEED", "reproducible-action-route")
Expand All @@ -38,6 +39,7 @@ def collect(argv):
assert args[args.index("--jets") + 1] == "3"
assert args[args.index("--carrier") + 1] == "classic"
assert args[args.index("--water") + 1] == "original"
assert args[args.index("--graph-theme") + 1] == "github"
assert "--no-auto-compress" in args
assert args[args.index("--seed") + 1] == "reproducible-action-route"
outputs = dict(line.split("=", 1) for line in (tmp_path / "outputs").read_text().splitlines())
Expand Down Expand Up @@ -91,6 +93,7 @@ def test_actual_action_script_generates_demo_assets_and_returns_paths(tmp_path,
"JET_JETS": "3",
"JET_CARRIER": carrier,
"JET_WATER": water,
"JET_GRAPH_THEME": "github",
"JET_AUTO_COMPRESS": "true",
"GITHUB_TOKEN": "never-save-this-test-token",
},
Expand All @@ -104,6 +107,7 @@ def test_actual_action_script_generates_demo_assets_and_returns_paths(tmp_path,
assert report["jets"] == 3 and len(report["flights"]) == 3
assert report["carrier"] == carrier
assert report["water"] == water
assert report["graph_theme"] == "github"
assert sum(f["attacks"] for f in report["flights"]) == report["attacks"]
assert report["decoded_frames_match"]
outputs = dict(line.split("=", 1) for line in (tmp_path / "outputs").read_text().splitlines())
Expand Down
9 changes: 8 additions & 1 deletion tests/test_auto_compress.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def fit(renderer, output, options, **kwargs):
assert renderer.flight.seed == "keep-this-route" and renderer.jet_count == 3
assert renderer.carrier == "classic"
assert renderer.water == "original"
assert renderer.graph_theme == "github"
assert len(renderer.flight.strikes) == 14
assert sum(s.cell.day.commits for s in renderer.flight.strikes) == calendar.total
if options.fps == 20:
Expand All @@ -107,7 +108,13 @@ def fit(renderer, output, options, **kwargs):
monkeypatch.setattr("git_jet.flight.secrets.token_hex", fresh_seed)
monkeypatch.setattr(exporter, "export", fit)
assert render_assets(
calendar, tmp_path, Options(), jets=3, carrier="classic", water="original"
calendar,
tmp_path,
Options(),
jets=3,
carrier="classic",
water="original",
graph_theme="github",
) == {"fps": 10}
assert attempts == [Options(2, 20), Options(1, 20), Options(1, 10)]
assert len(seeds) == 1
Expand Down
1 change: 1 addition & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def forbidden(*args, **kwargs):
assert report["jets"] == jets and len(report["flights"]) == jets
assert report["carrier"] == carrier
assert report["water"] == water
assert report["graph_theme"] == "blue"
assert sum(f["attacks"] for f in report["flights"]) == 2
assert {t["jet"] for t in report["targets"]}.issubset(set(range(1, jets + 1)))
renderer = Renderer(calendar, jets=jets, seed=report["seed"], carrier=carrier, water=water)
Expand Down
71 changes: 71 additions & 0 deletions tests/test_graph_theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pytest
from PIL import ImageColor

from git_jet.art import GITHUB_GRID, commit_tile
from git_jet.cli import main
from git_jet.demo import demo_calendar
from git_jet.flight import GRID_Y
from git_jet.render import Renderer


def test_blue_remains_the_default_for_tiles_and_scenes():
for level in range(5):
for destroyed in (False, True):
assert (
commit_tile(level, destroyed).tobytes()
== commit_tile(level, destroyed, theme="blue").tobytes()
)
default = Renderer(demo_calendar(), seed="graph-colors")
blue = Renderer(demo_calendar(), seed="graph-colors", graph_theme="blue")
assert default.graph_theme == "blue"
assert default.frame(0).tobytes() == blue.frame(0).tobytes()


@pytest.mark.parametrize("level", (1, 2, 3, 4))
def test_github_theme_uses_flat_green_activity_levels(level):
tile = commit_tile(level, theme="github")
green = ImageColor.getcolor(GITHUB_GRID[level], "RGBA")
assert tile.getpixel((3, 3)) == green
assert {color for _, color in tile.getcolors() if color[3]} == {green}


def test_github_destroyed_days_become_transparent_empty_cells():
empty = commit_tile(0, theme="github")
assert empty.getpixel((3, 3))[3] == 0
for level in range(1, 5):
assert commit_tile(level, True, theme="github").tobytes() == empty.tobytes()


def test_graph_theme_preserves_flight_targets_clearing_and_scene_geometry():
blue = Renderer(demo_calendar(), seed="graph-colors", graph_theme="blue")
green = Renderer(demo_calendar(), seed="graph-colors", graph_theme="github")
assert blue.flight.strikes == green.flight.strikes
assert blue.frame_count == green.frame_count
frames = {0, blue.frame_count - 1, *(s.hit for s in blue.flight.strikes)}
for frame in frames:
assert (
blue.grid_frame(frame).getchannel("A").tobytes()
== green.grid_frame(frame).getchannel("A").tobytes()
)
assert (
blue.frame(0).crop((0, 0, 480, GRID_Y)).tobytes()
== green.frame(0).crop((0, 0, 480, GRID_Y)).tobytes()
)
assert blue.grid_frame(0).tobytes() != green.grid_frame(0).tobytes()
assert green.grid_frame(green.frame_count - 1).tobytes() == green.grid_frame(0).tobytes()


@pytest.mark.parametrize("theme", ("unknown", None, [], True))
def test_invalid_graph_themes_are_rejected(theme):
with pytest.raises(ValueError, match="graph theme"):
Renderer(demo_calendar(), graph_theme=theme)


def test_cli_rejects_unknown_graph_theme_before_collecting(monkeypatch):
def forbidden(*args, **kwargs):
raise AssertionError("invalid graph theme must not collect data")

monkeypatch.setattr("git_jet.cli.GitHubClient.collect", forbidden)
with pytest.raises(SystemExit) as error:
main(["render", "--username", "octocat", "--graph-theme", "unknown"])
assert error.value.code == 2
Loading