Created
April 1, 2026 05:13
-
-
Save zmxv/7f83671f860c15be02f45b07fee207fc to your computer and use it in GitHub Desktop.
Claude Code buddy animation
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 python3 | |
| """Generate an animated GIF of all companion sprite animations.""" | |
| from PIL import Image, ImageDraw, ImageFont | |
| # --- Sprite data --- | |
| SPECIES = [ | |
| 'duck', 'goose', 'blob', 'cat', 'dragon', 'octopus', 'owl', 'penguin', | |
| 'turtle', 'snail', 'ghost', 'axolotl', 'capybara', 'cactus', 'robot', | |
| 'rabbit', 'mushroom', 'chonk', | |
| ] | |
| EYE = '·' | |
| BODIES = { | |
| 'duck': [ | |
| [' ', ' __ ', ' <(· )___ ', ' ( ._> ', ' `--´ '], | |
| [' ', ' __ ', ' <(· )___ ', ' ( ._> ', ' `--´~ '], | |
| [' ', ' __ ', ' <(· )___ ', ' ( .__> ', ' `--´ '], | |
| ], | |
| 'goose': [ | |
| [' ', ' (·> ', ' || ', ' _(__)_ ', ' ^^^^ '], | |
| [' ', ' (·> ', ' || ', ' _(__)_ ', ' ^^^^ '], | |
| [' ', ' (·>> ', ' || ', ' _(__)_ ', ' ^^^^ '], | |
| ], | |
| 'blob': [ | |
| [' ', ' .----. ', ' ( · · ) ', ' ( ) ', ' `----´ '], | |
| [' ', ' .------. ', ' ( · · ) ', ' ( ) ', ' `------´ '], | |
| [' ', ' .--. ', ' (· ·) ', ' ( ) ', ' `--´ '], | |
| ], | |
| 'cat': [ | |
| [' ', ' /\\_/\\ ', ' ( · ·) ', ' ( ω ) ', ' (")_(") '], | |
| [' ', ' /\\_/\\ ', ' ( · ·) ', ' ( ω ) ', ' (")_(")~ '], | |
| [' ', ' /\\-/\\ ', ' ( · ·) ', ' ( ω ) ', ' (")_(") '], | |
| ], | |
| 'dragon': [ | |
| [' ', ' /^\\ /^\\ ', ' < · · > ', ' ( ~~ ) ', ' `-vvvv-´ '], | |
| [' ', ' /^\\ /^\\ ', ' < · · > ', ' ( ) ', ' `-vvvv-´ '], | |
| [' ~ ~ ', ' /^\\ /^\\ ', ' < · · > ', ' ( ~~ ) ', ' `-vvvv-´ '], | |
| ], | |
| 'octopus': [ | |
| [' ', ' .----. ', ' ( · · ) ', ' (______) ', ' /\\/\\/\\/\\ '], | |
| [' ', ' .----. ', ' ( · · ) ', ' (______) ', ' \\/\\/\\/\\/ '], | |
| [' o ', ' .----. ', ' ( · · ) ', ' (______) ', ' /\\/\\/\\/\\ '], | |
| ], | |
| 'owl': [ | |
| [' ', ' /\\ /\\ ', ' ((·)(·)) ', ' ( >< ) ', ' `----´ '], | |
| [' ', ' /\\ /\\ ', ' ((·)(·)) ', ' ( >< ) ', ' .----. '], | |
| [' ', ' /\\ /\\ ', ' ((·)(-)) ', ' ( >< ) ', ' `----´ '], | |
| ], | |
| 'penguin': [ | |
| [' ', ' .---. ', ' (·>·) ', ' /( )\\ ', ' `---´ '], | |
| [' ', ' .---. ', ' (·>·) ', ' |( )| ', ' `---´ '], | |
| [' .---. ', ' (·>·) ', ' /( )\\ ', ' `---´ ', ' ~ ~ '], | |
| ], | |
| 'turtle': [ | |
| [' ', ' _,--._ ', ' ( · · ) ', ' /[______]\\ ', ' `` `` '], | |
| [' ', ' _,--._ ', ' ( · · ) ', ' /[______]\\ ', ' `` `` '], | |
| [' ', ' _,--._ ', ' ( · · ) ', ' /[======]\\ ', ' `` `` '], | |
| ], | |
| 'snail': [ | |
| [' ', ' · .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '], | |
| [' ', ' · .--. ', ' | ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '], | |
| [' ', ' · .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~ '], | |
| ], | |
| 'ghost': [ | |
| [' ', ' .----. ', ' / · · \\ ', ' | | ', ' ~`~``~`~ '], | |
| [' ', ' .----. ', ' / · · \\ ', ' | | ', ' `~`~~`~` '], | |
| [' ~ ~ ', ' .----. ', ' / · · \\ ', ' | | ', ' ~~`~~`~~ '], | |
| ], | |
| 'axolotl': [ | |
| [' ', '}~(______)~{', '}~(· .. ·)~{', ' ( .--. ) ', ' (_/ \\_) '], | |
| [' ', '~}(______){~', '~}(· .. ·){~', ' ( .--. ) ', ' (_/ \\_) '], | |
| [' ', '}~(______)~{', '}~(· .. ·)~{', ' ( -- ) ', ' ~_/ \\_~ '], | |
| ], | |
| 'capybara': [ | |
| [' ', ' n______n ', ' ( · · ) ', ' ( oo ) ', ' `------´ '], | |
| [' ', ' n______n ', ' ( · · ) ', ' ( Oo ) ', ' `------´ '], | |
| [' ~ ~ ', ' u______n ', ' ( · · ) ', ' ( oo ) ', ' `------´ '], | |
| ], | |
| 'cactus': [ | |
| [' ', ' n ____ n ', ' | |· ·| | ', ' |_| |_| ', ' | | '], | |
| [' ', ' ____ ', ' n |· ·| n ', ' |_| |_| ', ' | | '], | |
| [' n n ', ' | ____ | ', ' | |· ·| | ', ' |_| |_| ', ' | | '], | |
| ], | |
| 'robot': [ | |
| [' ', ' .[||]. ', ' [ · · ] ', ' [ ==== ] ', ' `------´ '], | |
| [' ', ' .[||]. ', ' [ · · ] ', ' [ -==- ] ', ' `------´ '], | |
| [' * ', ' .[||]. ', ' [ · · ] ', ' [ ==== ] ', ' `------´ '], | |
| ], | |
| 'rabbit': [ | |
| [' ', ' (\\__/) ', ' ( · · ) ', ' =( .. )= ', ' (")__(") '], | |
| [' ', ' (|__/) ', ' ( · · ) ', ' =( .. )= ', ' (")__(") '], | |
| [' ', ' (\\__/) ', ' ( · · ) ', ' =( . . )= ', ' (")__(") '], | |
| ], | |
| 'mushroom': [ | |
| [' ', ' .-o-OO-o-. ', '(__________)', ' |· ·| ', ' |____| '], | |
| [' ', ' .-O-oo-O-. ', '(__________)', ' |· ·| ', ' |____| '], | |
| [' . o . ', ' .-o-OO-o-. ', '(__________)', ' |· ·| ', ' |____| '], | |
| ], | |
| 'chonk': [ | |
| [' ', ' /\\ /\\ ', ' ( · · ) ', ' ( .. ) ', ' `------´ '], | |
| [' ', ' /\\ /| ', ' ( · · ) ', ' ( .. ) ', ' `------´ '], | |
| [' ', ' /\\ /\\ ', ' ( · · ) ', ' ( .. ) ', ' `------´~ '], | |
| ], | |
| } | |
| EYES = ['·', '✦', '×', '◉', '@', '°'] | |
| HATS = { | |
| 'crown': ' \\^^^/ ', | |
| 'tophat': ' [___] ', | |
| 'propeller': ' -+- ', | |
| 'halo': ' ( ) ', | |
| 'wizard': ' /^\\ ', | |
| 'beanie': ' (___) ', | |
| 'tinyduck': ' ,> ', | |
| } | |
| RARITIES = [ | |
| ('common', (120, 120, 150)), | |
| ('uncommon', (80, 200, 120)), | |
| ('rare', (100, 149, 237)), | |
| ('epic', (200, 120, 255)), | |
| ('legendary', (255, 200, 60)), | |
| ] | |
| RARITY_WEIGHT = {'common': 60, 'uncommon': 25, 'rare': 10, 'epic': 4, 'legendary': 1} | |
| BUDDY_COLORS = [ | |
| (255, 50, 50), (255, 165, 0), (255, 255, 0), | |
| (0, 200, 0), (50, 100, 255), (180, 50, 255), | |
| ] | |
| # --- Layout --- | |
| COL_W = 12 | |
| COLS = 6 | |
| FONT_SIZE = 16 | |
| FONT_PATH = '/System/Library/Fonts/Menlo.ttc' | |
| BG = (22, 22, 38) | |
| FG = (220, 220, 220) | |
| LABEL = (120, 120, 150) | |
| ACCENT = (255, 130, 160) | |
| PAD_X, PAD_Y = 24, 12 | |
| MARGIN = 32 | |
| SECTION_GAP = 32 | |
| TITLE_GAP = 8 | |
| FRAME_MS = 300 | |
| BLANK = ' ' * COL_W | |
| # --- Rendering --- | |
| def render_sprite(species, frame_idx): | |
| """Return lines for one animation frame, dropping unused hat slot.""" | |
| frames = BODIES[species] | |
| body = list(frames[frame_idx % len(frames)]) | |
| if not body[0].strip() and all(not f[0].strip() for f in frames): | |
| body = body[1:] | |
| return body | |
| def render_all(): | |
| """Pre-render all frames with bottom-aligned height normalization.""" | |
| rendered = [] | |
| for sp in SPECIES: | |
| sp_frames = [render_sprite(sp, f) for f in range(len(BODIES[sp]))] | |
| max_h = max(len(fr) for fr in sp_frames) | |
| for fr in sp_frames: | |
| while len(fr) < max_h: | |
| fr.insert(0, BLANK) | |
| rendered.append(sp_frames) | |
| global_h = max(len(fs[0]) for fs in rendered) | |
| for sp_frames in rendered: | |
| for fr in sp_frames: | |
| while len(fr) < global_h: | |
| fr.insert(0, BLANK) | |
| return rendered, global_h | |
| def draw_title(draw, tx, ty, font, char_w): | |
| """Draw the title line with rainbow /buddy.""" | |
| title = 'claude code buddies' | |
| draw.text((tx, ty), title, font=font, fill=ACCENT) | |
| sx = tx + (len(title) + 1) * char_w | |
| draw.text((sx, ty), '(type ', font=font, fill=FG) | |
| sx += 6 * char_w | |
| for ch, color in zip('/buddy', BUDDY_COLORS): | |
| draw.text((sx, ty), ch, font=font, fill=color) | |
| sx += char_w | |
| draw.text((sx, ty), ' to activate)', font=font, fill=FG) | |
| def draw_sprites(draw, all_rendered, frame_idx, cell_w, label_h, line_h): | |
| """Draw the sprite grid.""" | |
| cell_h = label_h + max(len(fs[0]) for fs in all_rendered) * line_h + PAD_Y * 2 | |
| eye = EYES[frame_idx % len(EYES)] | |
| for i, sp in enumerate(SPECIES): | |
| col, row = i % COLS, i // COLS | |
| x0 = MARGIN + col * cell_w + PAD_X | |
| y0 = MARGIN + line_h + SECTION_GAP + row * cell_h + PAD_Y | |
| draw.text((x0, y0), sp, font=draw._default_font, fill=LABEL) | |
| fc = len(BODIES[sp]) | |
| for j, line in enumerate(all_rendered[i][frame_idx % fc]): | |
| draw.text((x0, y0 + label_h + j * line_h), line.replace(EYE, eye), font=draw._default_font, fill=FG) | |
| def draw_hats(draw, y, font, cell_w, line_h): | |
| """Draw hats in two rows (4+3) aligned with grid columns.""" | |
| x_left = MARGIN + PAD_X | |
| draw.text((x_left, y), 'hats', font=font, fill=ACCENT) | |
| y += line_h + TITLE_GAP | |
| hat_items = list(HATS.items()) | |
| for ri, row_hats in enumerate([hat_items[:4], hat_items[4:]]): | |
| if ri > 0: | |
| y += line_h | |
| for ci, (name, art) in enumerate(row_hats): | |
| hx = MARGIN + ci * cell_w + PAD_X | |
| draw.text((hx, y), name, font=font, fill=LABEL) | |
| draw.text((hx, y + line_h * 2), art, font=font, fill=FG) | |
| y += line_h * 3 | |
| return y | |
| def draw_rarity(draw, y, font, char_w, cell_w, line_h): | |
| """Draw rarity bar chart with legend.""" | |
| x_left = MARGIN + PAD_X | |
| draw.text((x_left, y), 'rarity', font=font, fill=ACCENT) | |
| y += line_h + TITLE_GAP | |
| bar_w = COLS * cell_w - 2 * PAD_X | |
| bar_h = line_h + 4 | |
| total = sum(RARITY_WEIGHT.values()) | |
| bx = x_left | |
| for name, color in RARITIES: | |
| seg_w = max(1, int(bar_w * RARITY_WEIGHT[name] / total)) | |
| draw.rectangle([bx, y, bx + seg_w - 1, y + bar_h - 1], fill=color) | |
| bx += seg_w | |
| y += bar_h + TITLE_GAP | |
| lx = x_left | |
| for name, color in RARITIES: | |
| sw = char_w | |
| sy = y + (line_h - sw) // 2 | |
| draw.rectangle([lx, sy, lx + sw - 1, sy + sw - 1], fill=color) | |
| lx += sw + char_w // 2 | |
| label = f'{name} {RARITY_WEIGHT[name]}%' | |
| draw.text((lx, y), label, font=font, fill=color) | |
| lx += (len(label) + 2) * char_w | |
| return y + line_h | |
| def calc_static_height(line_h): | |
| """Height of hats + rarity sections.""" | |
| h = SECTION_GAP + line_h + TITLE_GAP # hats title | |
| h += line_h * 3 + line_h + line_h * 3 # two hat rows + gap | |
| h += SECTION_GAP + line_h + TITLE_GAP # rarity title | |
| h += line_h + 4 + TITLE_GAP + line_h + PAD_Y # bar + legend + padding | |
| return h | |
| def draw_frame(frame_idx, all_rendered, max_h, font, char_w, line_h): | |
| """Render one animation frame as a PIL Image.""" | |
| label_h = line_h + 4 | |
| cell_w = COL_W * char_w + PAD_X * 2 | |
| cell_h = label_h + max_h * line_h + PAD_Y * 2 | |
| n_rows = (len(SPECIES) + COLS - 1) // COLS | |
| title_h = line_h + SECTION_GAP | |
| grid_h = n_rows * cell_h | |
| img_w = COLS * cell_w + MARGIN * 2 | |
| img_h = title_h + grid_h + calc_static_height(line_h) + MARGIN * 2 | |
| img = Image.new('RGB', (img_w, img_h), BG) | |
| draw = ImageDraw.Draw(img) | |
| draw._default_font = font | |
| draw_title(draw, MARGIN + PAD_X, MARGIN + PAD_Y, font, char_w) | |
| eye = EYES[frame_idx % len(EYES)] | |
| for i, sp in enumerate(SPECIES): | |
| col, row = i % COLS, i // COLS | |
| x0 = MARGIN + col * cell_w + PAD_X | |
| y0 = MARGIN + title_h + row * cell_h + PAD_Y | |
| draw.text((x0, y0), sp, font=font, fill=LABEL) | |
| fc = len(BODIES[sp]) | |
| for j, line in enumerate(all_rendered[i][frame_idx % fc]): | |
| draw.text((x0, y0 + label_h + j * line_h), line.replace(EYE, eye), font=font, fill=FG) | |
| y = MARGIN + title_h + grid_h + SECTION_GAP | |
| y = draw_hats(draw, y, font, cell_w, line_h) | |
| y += SECTION_GAP | |
| draw_rarity(draw, y, font, char_w, cell_w, line_h) | |
| return img | |
| def main(): | |
| font = ImageFont.truetype(FONT_PATH, FONT_SIZE) | |
| bbox = font.getbbox('M') | |
| char_w, char_h = bbox[2] - bbox[0], bbox[3] - bbox[1] | |
| line_h = char_h + 4 | |
| all_rendered, max_h = render_all() | |
| frames = [draw_frame(f, all_rendered, max_h, font, char_w, line_h) for f in range(len(EYES))] | |
| out = 'sprites.gif' | |
| frames[0].save(out, save_all=True, append_images=frames[1:], duration=FRAME_MS, loop=0) | |
| print(f'Saved {out} ({frames[0].size[0]}x{frames[0].size[1]}, {len(frames)} frames)') | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment